Flint 0.5#9
Open
irzhywau wants to merge 552 commits into
Open
Conversation
…fix BUG-1 zombie reaping Re-integrates the parked W2 consent work onto the hardened flint line and fixes the BUG-1 VM-lifecycle leak, as a verified slice ahead of the step-6 grant crux. BUG-1 (zombie reaping): RunningVm::has_exited() reaps a self-exited crosvm child via try_wait() (a zombie still answers kill(pid,0), so the liveness-only sweep never collected it). reap_dead_capsules now iter_mut + has_exited so it actually reaps; is_running() stays a cheap &self probe for read-only status queries. New KVM-free unit tests cover the reaping decision. W2 step 3 (binding): AffordanceBinding + four Option binding fields (capsule/principal_id/method_id/input_hash) on PendingCapabilityRequest, plus create_affordance_request. Reconciled with flint's existing requester_capsule_id (G-ID): a single create_request_inner now carries BOTH the requester identity (who asked) and the affordance binding (what consent is scoped to); neither overwrites the other. Tests prove they coexist through a store round-trip. W2 step 4 (consent path): shared elastos_common::canonical_input_hash (key-order independent so gateway and runtime agree); /api/capability/request accepts the four binding fields and routes an all-four-present request through create_affordance_request, rejecting a partial set fail-closed; the gateway's flat 403 for User-approval / high-risk affordances is replaced by an InvocationGate that POSTs a scoped consent request to the runtime over HTTP (the runtime remains the sole key holder; the edge only terminates protocol). Behaviour-neutral for existing session-capability callers (all binding fields default None). Gates: elastos-crosvm 22, elastos-runtime capability::pending 19, elastos-server lib 850 pass; fmt + clippy -D warnings clean on touched crates. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…-consent enforcement crux) When a pending affordance-consent request is approved, the grant now READS its binding and mints a token scoped to the EXACT affordance + arguments, distinct from the ordinary session/capability grant (which keeps flint's G-ID path): - TokenConstraints gains method_id + input_hash (sealed pub(crate) fields) and a for_affordance() constructor (single-use, non-delegatable, bound). The binding is hashed into CapabilityToken::signable_bytes, so the (method_id, input_hash) the user approved is cryptographically sealed into the token — it cannot be replayed against a different method or with different arguments. Proven by a tamper test: swapping either field fails signature verification. - The grant handler branches on the request binding: an affordance request mints at the BOUND capsule (request.capsule), single-use, +1h expiry; an ordinary request keeps the requester_capsule_id (G-ID) session path unchanged. A handler test proves the granted token carries the binding and validates only at the bound capsule. Roadmap updated: W2 now ~60% (steps 1-4 re-integrated, 5 by construction, 6 done; 7-11 remain). Notes the binding-capsule identity-domain follow-up for step 10. Gates: elastos-runtime + elastos-server lib tests pass (incl. 3 new W2 tests); fmt + clippy -D warnings clean on touched crates. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…al consent identity (redemption enforcement)
The runtime becomes the SOLE validator for affordance-consent tokens. A capsule
redeems its granted token for a single invocation; the edge cannot bypass it.
- POST /api/capability/validate-and-consume (authenticated, not shell-only): the
caller identity is the AUTHENTICATED session's vm-{name} (never the request
body — Principle 16). It decodes the token, requires it to be an
affordance-consent token (carries a binding), re-checks the EXACT method and
re-hashes the arguments (shared canonical_input_hash) against the sealed
binding, THEN runs capability_manager.validate() — which performs the full 12
checks AND the atomic single-use consume + signed CapabilityUse. The binding
checks precede the consume, so a mismatch fails closed and never burns the use.
Every failure returns a distinct code with no internal prose (no topology leak).
- Identity domain (step 7a): consent now binds the canonical "vm-{name}" (the
G-ID founder-ratified identity every gate validates against), not the bare
manifest name — so the affordance token lives in the ONE identity domain rather
than a second one (the anti-pattern G-ID already eliminated).
- Test proves the full request -> grant -> redeem path: a correct redemption
consumes once, replay is refused, and method-swap / args-swap / wrong-caller
each fail closed WITHOUT burning the single use.
Roadmap: W2 ~70% (steps 1-7 done; 8-11 remain). Gates: elastos-server lib 852
pass; clippy -D warnings + fmt clean; alignment-check OK.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…consent dispatch (compiler-enforced)
"No consent-gated dispatch without a live, redeemed grant" becomes a property the
COMPILER enforces, not a call-order convention.
- ValidatedAffordanceGrant: an unforgeable witness with a module-private field —
the ONLY constructor is redeem_affordance_grant on a successful
validate-and-consume. dispatch_consented_affordance requires it BY VALUE, so a
consent-gated affordance cannot be dispatched without proof of redemption; it
also re-asserts the grant authorises this exact method (defence in depth).
- plan_affordance_dispatch(gate, has_consent_token): a pure, total, unit-tested
routing decision — Direct / RaiseConsent / RedeemThenDispatch. The invoke
handler now drives off it: low-risk dispatches directly; a consent-gated first
call raises a consent request (202); a consent-gated retry (token presented)
redeems then dispatches.
- redeem_affordance_grant forwards the CALLER's own authorization to
validate-and-consume, so the runtime authenticates the redemption as the bound
capsule (vm-{name}) — not the shell. Any non-success fails closed: no witness,
therefore no dispatch.
Honest scope: the live identity round-trip is fail-closed and is verified
end-to-end by the step-10 journey test; this slice lands the structural gate.
Roadmap: W2 ~80% (steps 1-8 done; 9-11 remain). Gates: elastos-server lib 853
pass; clippy -D warnings + fmt clean; alignment-check OK.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ing audit for affordance use The redemption now produces a portable, verifiable receipt AND fails closed unless a durable signed use-record was written — "if there's no receipt, there's no act" at the protocol layer. - AffordanceGrantReceiptV1 (new capability/receipt.rs): runtime-signed (the capability issuer key, the trust root) attestation binding the exact (capsule, method_id, input_hash, resource, action, token_id, redeemed_at). Domain-separated, length-prefixed signable digest; verify() fails closed on any malformed signature. Tests: a fresh receipt verifies under the issuer key; tampering ANY bound field breaks verification; a different key does not verify. - Blocking audit: in CapabilityManager::validate(), an affordance (bound, single-use) token's success path emits the signed durable CapabilityUse via emit() and fails closed with the new ValidationError::AuditWriteFailed if it cannot be written (mirrors the AUD-2/AUD-3 fail-closed audit fixes). Ordinary capability tokens keep the existing best-effort emit — no behaviour change for any other caller. - validate-and-consume returns the signed receipt; AuditWriteFailed maps to 503 (a server fault), distinct from the caller-authorization 403s. The redemption test now asserts the returned receipt verifies under the runtime key and binds the exact (method, args, capsule). Decision: runtime-sign (the runtime holds the issuer key and is the trust root). Follow-up: sign the gateway provider-effect telemetry envelope (signer_did:None) — the authoritative attestation is this runtime-signed receipt. Roadmap: W2 ~90% (steps 1-9 done; 10 journey + 11 alignment/docs remain). Gates: elastos-runtime capability 128 pass (3 receipt tests); elastos-server lib 853 pass; clippy -D warnings + fmt clean; alignment-check OK. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…+ fail-closed branches
test_affordance_consent_journey walks the real loop with live components
(CapabilityManager + PendingRequestStore): a consent-gated request -> grant ->
redeem -> a signed receipt that VERIFIES under the runtime capability key and
binds the exact (capsule, method, args). Then every "no" path fails closed:
- Deny: the user refuses; the request is denied fail-closed (no redeemable token).
- Revoked: a grant revoked before redemption cannot be redeemed.
- Not-an-affordance-token: an ordinary session token (no binding) is refused at
validate-and-consume (it must use the ordinary capability gate).
The (method-swap, arg-swap, wrong-caller, replay) matrix is covered by
validate_and_consume_enforces_binding_single_use_and_caller; signature / expiry /
revocation primitives are enforced inside validate() (capability-conformance
battery) and flow through validate-and-consume unchanged.
Honest scope: the gateway->runtime HTTP redeem round-trip runs against the test
harness's stub runtime, so the live forwarded-bearer -> vm-{name} round-trip
remains an integration check, not unit-verified.
Roadmap: W2 ~95% (steps 1-10 done; 11 alignment+docs remains, then W2 CLOSED).
Gates: elastos-server lib 854 pass; clippy -D warnings + fmt clean; alignment OK.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…e; CLOSE W2
W2 (unstub the consent act path) is complete and now un-regressable. Five scoped
ratchet assertions in check-wci-alignment.sh fail the build if any W2 guarantee
silently regresses:
- the flat 403 consent stub must not return to gateway_capsule_catalog.rs (scoped
to source so the doc that quotes the old string is exempt);
- validate-and-consume must stay the registered redemption route (server.rs);
- consent dispatch must go through dispatch_consented_affordance + the
ValidatedAffordanceGrant witness (gateway);
- affordance use must fail closed via AuditWriteFailed (manager.rs).
Each assertion is non-vacuous (patterns confirmed present/absent on the current
tree) and the gate stays green.
Docs: ROADMAP.md and W2_CONSENT_PLAN.md mark W2 CLOSED (steps 1-11) and record the
one open follow-up (sign the gateway provider-effect telemetry envelope;
signer_did:None) — the authoritative attestation is already the runtime-signed
receipt. The live gateway->runtime forwarded-bearer->vm-{name} redeem round-trip
is integration-verified, not unit-tested.
W2 summary: consent is real, cryptographically enforced, single-use + bound,
witness-gated at dispatch, blocking-audited, receipted, and pinned. Next: W0/W1
(make the halo a core-computed fact). Gates: alignment-check OK; no Rust changed
this slice (elastos-server lib 854 pass stands from step 10).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…gine)
Make blast-radius a COMPUTED fact, not a self-rating. ReachDescriptorV1::derive
computes reach from the capsule's ENFORCED capability (isolation tier +
guest_network/carrier net access) and the affordance's concrete (resource,
operation) — never from the self-declared AffordanceRisk, which a capsule can set
to anything. This is the data the shell's halo will render, honest by
construction.
- ReachDescriptorV1 { egress(None|Open), isolation(Data|Wasm|MicroVm|HostProcess),
scope(Object|Collection|System|Unknown), reversibility(Reversible|OneWay|
Unknown), observed }. observed=false when an affordance is resource-less or
operation-less, so a vague act renders "incomplete", never falsely cool.
- declared_understates_reach(&AffordanceRisk): the advisory "a clone must lie"
detector — flags a capsule claiming low risk while the observed reach is far
(open egress, irreversible, or system-wide). Declared risk stays advisory; the
core stamps the observed reach and only flags the contradiction (does not gate).
- 6 tests: sandboxed read reaches nowhere; networked microVM delete over a
collection reaches far; a carrier service is a host process with open egress; a
scheme wildcard is system scope; resource/operation-less affordances are not
fully observed; declared-low-but-far-reaching is flagged.
Schema is V1 and additive — it is the shell's reach contract (and feeds ESP v0 in
W4). Next (W0b): wire derive() into the inspect/plan projection the shell reads;
W1 adds EgressReach::Allowlisted (per-host egress-as-capability).
Roadmap: W0 IN PROGRESS (engine landed). Gates: elastos-common 92 pass (6 reach
tests); clippy -D warnings + fmt clean; alignment-check OK.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ell reads
Every declared affordance in the capsule catalog now carries its CORE-DERIVED
reach, so a shell can render the blast-radius halo from real state rather than the
self-declared risk.
- AffordanceReachView { interface_id, method_id, risk, reach, declared_understates_reach }
is attached as a parallel `affordance_reach` list on the catalog entry,
computed in catalog_capsule_summary via ReachDescriptorV1::derive from the
manifest's isolation type + ENFORCED permissions and each method's concrete
(resource, operation). The manifest descriptor (CapsuleAffordanceDescriptor)
stays a pure declaration — reach is projected alongside it, never mutated in.
- The declared-understates-reach flag rides the projection, so the "a clone must
lie" contradiction (claims low, reaches far) is visible to whatever renders it.
Test: a capsule whose `scan.all` declares risk=read over `elastos://*` projects
egress=none / isolation=wasm / scope=system / reversibility=reversible /
observed=true AND declared_understates_reach=true; a contained `open.one` over a
specific resource projects scope=object and is not flagged.
Roadmap: W0 CLOSED (engine + projection). Next: W1 (egress-as-capability adds
EgressReach::Allowlisted + enforcement at the net boundary). Gates: elastos-server
lib 855 pass; clippy -D warnings + fmt clean; alignment-check OK.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ail-closed)
The reach vocabulary now distinguishes a LEASHED egress from a wide-open one —
the moat point: a verified capsule with Open egress can be visibly more dangerous
than one scoped to a handful of hosts.
- EgressReach gains Allowlisted (between None and Open).
- EgressAllowlist { allowed_hosts, allowed_schemes } with a fail-closed permits():
a (host, scheme) is permitted ONLY if both are explicitly listed; an empty
list permits nothing, so "no grant" and "grant nothing" both deny.
- EgressReach::resolve(has_net_capability, Option<&EgressAllowlist>) is honest and
fail-closed: no capability -> None; capability + non-empty allow -> Allowlisted;
capability + no/empty allow -> Open (surfaced as the dangerous reading, never
quietly downgraded).
- is_far_reaching already keys on Open only, so a leashed (allowlisted) egress is
correctly NOT far-reaching while an open one is.
HONEST SCOPE: this is the egress-capability MODEL + its fail-closed matcher.
Enforcing it at the kernel network boundary (the crosvm TAP firewall) and
threading a bound allow-list through the launch path is W1b — it requires
/dev/kvm + CAP_NET_ADMIN, which this sandbox host lacks, so it is deliberately
deferred to a host that has them rather than faked. WASM is already
default-no-internet by construction (no socket surface in its WasiCtx).
Roadmap: W1 IN PROGRESS (model landed; W1b = boundary firewall on the KVM lane).
Gates: elastos-common 95 pass (3 new egress tests); clippy -D warnings + fmt
clean; alignment-check OK.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…l token) The privileged shell session token is no longer tied to the magic capsule name "shell" — it goes to the user-selected ACTIVE shell, and only if that capsule holds the Shell role. - shell_token_eligible(name, role, active_shell) = role == Shell && name == active_shell. Fail-closed: a non-Shell capsule can NEVER receive the shell token even if the active-shell pointer names it. - Supervisor gains an `active_shell` pointer (default "shell" => behaviour-neutral) with set_active_shell()/active_shell(), so a different Shell-role capsule can be the active shell without the runtime hardcoding a name — the backend the shell-picker (W6) selects against. - The launch site swaps `name == "shell"` for the role-based decision; the bundled shell (role=shell) is unaffected. Test: the bundled shell stays eligible; a different Shell-role capsule selected as the active shell is eligible; a non-Shell capsule is never eligible; a Shell-role capsule that is not the active shell is not eligible. Deferred (W3b): rename the headless "shell" decision-engine to "consent-broker" (a wide, mechanical rename — safer as its own clearly-scoped slice). Roadmap: W3 IN PROGRESS (shell de-hardcoded; consent-broker rename = W3b). Gates: elastos-server lib 856 pass; clippy -D warnings + fmt clean; alignment-check OK. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…m shipped state ESP (the ElastOS Shell Protocol) v0: the read-only projection contract a shell renders. Written by EXTRACTION — every fact/verb cites the exact struct + route that already produces it; nothing is invented. - docs/ESP_V0.md: the inviolable read-only-projection rule; the real transport (HTTP GET JSON + the 202 consent flow); per-fact `schema`-tag versioning; must-ignore-unknown for FACTS with the honest caveat that verb INPUTS use deny_unknown_fields; the 5 fact families (catalog/v1, interfaces/v1, reach.v1, affordance-consent-pending/v1, affordance.receipt.v1) with routes + source structs; the 4 verbs (request/grant/deny/validate-and-consume + invoke); and an initialize handshake DEFINED but marked not-yet-implemented. Degraded surfaces called out: EgressReach::Allowlisted modeled-not-enforced (W1b), ReachDescriptor.observed, the redeem->dispatch live round-trip (integration-only). - elastos/esp/esp_v0.ts + README: shared TS types mirroring the serde shapes (snake_case enum wire values; optional = skip_serializing_if/default; fact types carry an index signature for must-ignore-unknown). Type-checks clean under `tsc --noEmit --strict`. - check-wci-alignment.sh: W4 ratchet — the schema tags + routes ESP_V0.md documents must still exist where it cites them, so the spec cannot silently drift from what the runtime serves. No Rust behaviour change (doc + types + script only) — elastos-server 856 / elastos-common 95 suites stand. Roadmap: W4 DONE; next W5 (the v1 projection shell + hero dDRM act). Gates: tsc --noEmit --strict clean; alignment-check OK. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…P-pure, in-cloud tested) The first pixel of the product, as verifiable projection logic: a headless, ESP-pure client that renders the never-seen two-channel object from REAL ESP v0 fact shapes — it holds no key and re-derives no crypto, it projects the runtime's verdicts (read-only projection principle). - two_channel.ts: Channel 1 trustMaterial(capsule) projects the runtime's trust_state verdict (verified | content_addressed | unsigned; fail-honest: unknown -> unsigned, never over-trusted). Channel 2 blastRadius(reach) projects the core-computed reach into a hazard band (open egress is high-hazard; a LEASHED/allowlisted egress reads cooler than an open one — the W1 point), honest about `observed` (unobserved -> incomplete, never falsely cool). twoChannel() composes them with refuseTrained (verified && hot) + declaredUnderstatesReach. - consent_act.ts: the hero dDRM act shapes — consentPendingIsWellFormed and receiptMatchesRequest (the receipt must attest the EXACT act requested; the runtime did the crypto, the shell asserts the binding). - 11 node:test cases prove it, headlined by "a VERIFIED capsule can read MORE dangerous than an UNSIGNED one" — you can refuse the thing the green check trained you to trust — and the hero act's receipt-binds-the-act safety. - Toolchain: tsconfig (strict + noUnusedLocals/Parameters + noImplicitReturns), a tiny node-shims.d.ts so it type-checks without @types/node, package.json (type: module), build/ gitignored. `tsc` clean; `node --test` 11/11. W5b (the visual Svelte shell) is the browser/local lane. No Rust change (esp_v0.ts gained the real trust_state/signature_state/cid_state fields) — the 856/95 suites stand. Gates: tsc strict + node --test 11 pass; alignment OK. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…il-closed) "A shell among shells", made selectable — pure ESP projection over the catalog facts, no Rust change (the catalog already emits role + launchable). - shell_picker.ts: isShellSelectable(capsule) = role=="shell" && launchable — the ESP mirror of the runtime's shell_token_eligible, so a non-shell can NEVER become the active shell. selectableShells(catalog), shellTrustCard (reuses the two-channel trust-material so a user picks a shell knowing its trust verdict), shellPicker(catalog, active?) (defaults to first selectable; a stale/non-shell active falls back fail-closed), withActiveShell (returns null for a non-shell). - refraction.ts: RefractionState<T> — two registered shells over ONE projected state; toggleFocus swaps the lens and carries the projected state through UNCHANGED (one source of authority, no migration). The verifiable model under W5b's ~320ms visual cross-fade. - 21 node:test (with two_channel's): only shell+launchable selectable; non-shell never; picker default/select/fallback; the toggle preserves + round-trips the projected state. node-shims.d.ts gained deepEqual/strictEqual. The picker only RENDERS + REQUESTS; the runtime's set_active_shell (W3a) is the sole thing that actually switches authority (ESP read-only principle). No Rust change — the 856/95 suites stand. Gates: tsc strict + node --test 21 pass; alignment OK. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…fact (the flywheel turns) The same signed receipt that delights the consumer IS the compliance evidence a regulator/insurer forces — re-projected, fail-closed. - ai_act_audit.ts: toAiActAuditRecord(consent, receipt) maps a redemption onto the controls an auditor checks -> AiActAuditRecordV1 (schema elastos.audit.ai-act.v1): the `act` (from the signed receipt), `human_oversight` (EU AI Act Art 14 — required for any user-approval OR high-risk class), `record_keeping` (Art 12 — the tamper-evident signed receipt), and a plain-language control mapping. - containmentEvidence(record): fail-closed at the audit layer — Art 12 fails on an UNSIGNED record (no receipt -> no provable act); Art 14 fails when a high-risk or user-approval act executed without human consent. `contained` requires both. - 26 node:test (with the rest of esp/): the mapping; a user-approved+signed act is contained; an unsigned record fails Art 12; a high-risk runtime-policy act (no human) fails Art 14; a low-risk automated act with a signed record is contained. This closes the in-cloud core of the full W0-W7 wedge sequence: consent is real and receipted (W2), the halo is truthful (W0), egress is modeled (W1a), the shell is de-hardcoded and selectable (W3a/W6), the protocol is written (W4), the two-channel object renders (W5a), and the receipt is now the compliance artifact (W7). Pure projection, no Rust change — the 856/95 suites stand. Gates: tsc strict + node --test 26 pass; alignment OK. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ide shell rename) The consent decision-engine was implicitly conflated with the capsule role `Shell`: `shell_only_middleware` gated capability grant/deny/revoke + lifecycle endpoints by calling `session.is_shell()` directly. A naive wide "shell → consent-broker" rename was NOT cleanly shippable — the trust boundary is not a separable entity from the role (KNOWN_GAPS G-CIE: "the grant root IS the trusted shell"), so renaming the role is forbidden and renaming only the middleware would leave a misleading `consent_broker` gate still calling `is_shell()`. Instead, name the decision-engine at one documented seam: - add `Session::is_consent_broker()` — the consent-authorization predicate, delegating to `is_shell()` today with a doc bridge; a future dedicated consent-broker tier changes only this one method. - rename `shell_only_middleware → consent_broker_only_middleware`, routing it through `is_consent_broker()`; update the import + 4 application sites + 403 message + the 2 doc-comment references + the KNOWN_GAPS G-CIE note. Pure clarity, no behavior change: the predicate is byte-equivalent today, and a capsule session is still fail-closed NOT a consent broker (new unit test pins both the equivalence and the fail-closed case). The capsule role `Shell`, `active_shell`, and `shell_token_eligible` are untouched. Gates: rustfmt --check, clippy -D warnings (runtime+server), session/middleware/ capability tests green, WCI alignment OK. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…on (DoS)
All three carrier bridges checked MAX_LINE_BYTES only AFTER `read_line` /
`.lines()` had already buffered the whole line, so an untrusted guest could OOM
the host by sending a line with no newline (the WASM API bridge had no bound at
all). The size check was decorative — the allocation it guarded already happened.
Add a fail-closed bounded reader that caps allocation DURING the read:
- `read_bounded_line` (async, AsyncBufRead) + `read_bounded_line_sync` (BufRead),
sharing a `BoundedLine { Line | Eof | TooLarge }` outcome. Each uses
`take(MAX+1).read_until(b'\n')` so the read itself stops at the cap, then
drains to the next newline IN BOUNDED CHUNKS (DRAIN_CHUNK_BYTES) so the stream
realigns to the following request without reintroducing the OOM.
- wired into all three loops: the async serial carrier bridge (the BUG-6-named
`read_line`), the sync WASM carrier bridge, and the sync WASM API bridge.
Test-first (in-memory pipes, no VM): `read_bounded_line_*` prove a complete line
reads, CRLF is stripped, an oversized line is rejected TooLarge AND the stream
realigns so the next request parses, oversized-at-EOF terminates cleanly, and the
sync twin enforces the same bound + realign.
Gates: cargo test (36 carrier_bridge tests green), clippy -D warnings,
rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-6 + ROADMAP updated.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
`next_cid` advanced with an unchecked `*next += 1` and never consulted the live VM set, so once the counter wrapped (overflow) it could re-hand a vsock CID a running VM still held — two VMs sharing a CID breaks vsock routing. Replace with a pure `allocate_cid(from, in_use) -> Option<(cid, advance)>`: - floors the transport-reserved low CIDs (0/1/2) so guests start at MIN_GUEST_CID (3); - skips any CID currently in the `running` map's `vsock_cid` set; - wraps the u32 space via `checked_add().unwrap_or(MIN_GUEST_CID)` instead of overflowing; - bounded scan (`in_use.len() + 1` candidates — pigeonhole) and returns None to fail closed if every CID is occupied. The launch site now holds the counter lock across the live-set scan (serializes concurrent launches) and refuses the launch on None rather than colliding. Test-first (pure, VM-free): `supervisor::tests::allocate_cid_*` prove the reserved floor, skip-live, wrap-without-overflow, the wrap-does-not-collide-with- a-live-VM case (the bug), and free-within-bound. Gates: cargo test (29 supervisor tests green), clippy -D warnings, rustfmt --check. KNOWN_GAPS BUG-8 updated. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
… final poll The carrier `request_capability` loop was sleep-then-check, so a grant recorded between the last in-loop poll and loop exit was never observed — the capsule got a spurious timeout even though the consent broker had granted. Extract `await_capability_decision` + a `CapabilityDecision` enum: poll the pending store on the interval, then do ONE final read after the loop so a late-landing decision is caught. The carrier bridge now routes its grant/deny/ expire/timeout handling through it (same observable responses). Test-first + deterministic (no timing flake): with `max_polls = 0` the loop body never runs, so a pre-seeded grant can ONLY be found by the trailing read — `await_capability_decision_catches_a_grant_after_the_loop` would time out under the old code. A clean-timeout case guards against a false grant. Residual (noted in KNOWN_GAPS): the WASM API bridge's HTTP poll has the same shape; its trailing-read fix needs an HTTP-mock test to gate and is a tracked follow-up (separate transport, not the named carrier loop). Gates: cargo test (38 carrier_bridge tests green), clippy -D warnings, rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-5 + ROADMAP updated. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…g failure
A single-use capability is consumed at validate() before send_raw, so a provider
failure burned the grant on a no-op (the holder had to re-consent for a failure
that wasn't their fault).
An object-capability audit established that refunding on a general provider Err
is UNSAFE: no carrier provider op is atomic on its Err path (write-then-fail is
possible) and ProviderError::{Provider,NotFound,Io} cannot distinguish "acted
then failed" from "never acted" — so a refund there could enable a second
execution of a partially-applied write (catastrophic for the actuator/payment
classes that are the point of the AI-Act containment story).
Land the one PROVABLY-safe slice: refund ONLY on ProviderError::NoProvider (a
routing failure — the registry invoked nothing), via a new saturating
CapabilityStore::refund_token_use + CapabilityManager::refund_use. Every other
failure keeps the use consumed (fail-closed). The carrier dispatch binds the
consumed token id once (single-assignment; we only reach dispatch with a
validated, consumed token) and refunds it on the NoProvider arm.
Test-first:
- capability::store::refund_token_use_is_the_saturating_inverse_of_try_use —
the core mechanic (returns exactly one slot; saturates at 0, never mints uses).
- carrier_bridge::carrier_invoke_refunds_single_use_grant_on_missing_provider —
the SAME single-use token reaches the provider lookup TWICE through the public
handle_request path; under the old burn-on-failure code the second call would
be capability_denied.
Residual (deferred, bigger refactor, recorded in KNOWN_GAPS): op-failure refunds
need a provider "did-not-act" signal before they can be made safe.
Gates: cargo test (39 carrier_bridge + 5 store green), clippy -D warnings,
rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-4 + ROADMAP updated.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
Define docs/ESP_SHELL_UI.md: every Svelte component the shell needs, mapped onto the proven, tested ESP projection it renders — the UI adds no new logic, it only projects what the headless esp/*.ts layer already computed (W5a/W6/W7). - The one invariant (every pixel a read-only projection of signed runtime state; no component holds a key, token, or authority; actions emit INTENTS the runtime gates, never authority the view mints). - Eight component contracts (props = ESP fact type + schema tag; the headless fn it projects; what it renders; the intent it may emit): CapsuleCard, TwoChannelObject (refuseTrained = verified && hot), HazardMeter, ConsentSheet, ReceiptBadge, ShellPicker, RefractionToggle, AiActAuditCard (Art 12/14 contained). - Composition tree + a schema-tag pinning table so the alignment ratchet can later guard the pixel⇄fact mapping, mirroring the W4 ESP route pins. Alignment ratchet gains W5b pins (the AiActAuditCard fact tag must exist in esp/ai_act_audit.ts; the spec must define the card and codify the no-authority invariant). The live Svelte paint + visual snapshot test stay on the browser/ local lane (tracked as W5b implementation in ROADMAP). Gates: WCI alignment OK (new W5b pins pass); ESP TS unchanged. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
A team code-quality audit (Kent Beck seat) over the campaign diff, each finding adversarially verified before applying: - F1 (confirmed): the `request_too_large` reply was hand-built in all three bridge loops with drifting formatting. Extract one `oversized_request_error()` so the wire shape cannot diverge between the microVM, WASM-carrier, and WASM-API bridges. - F2 (already satisfied): the sync bounded reader already documents itself as the "synchronous twin of read_bounded_line … same semantics" — the divergence-catching cross-reference the finding asked for. No change needed. - F3 (corrected): the finding claimed the WASM-API HTTP poll has "no bug — the last iteration is the final check." That premise is FALSE: the loop shares BUG-5's post-last-poll window (it is the tracked BUG-5 residual). Added an HONEST comment pointing at KNOWN_GAPS, NOT the finding's incorrect "fine" framing. No behavior change. Gates: cargo test (39 carrier_bridge green), clippy -D warnings, rustfmt --check. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
… safe refund Close the BUG-4 residual's MECHANISM: providers can now signal a provably-pre- effect rejection, and the carrier safely refunds the single use on it. - Add `ProviderError::DidNotAct(String)` with a load-bearing ocap contract on the variant: a provider may return it ONLY when it is certain nothing was mutated/ emitted/spent (precondition/validation failure), so a replay is an idempotent no-op. Any failure that MIGHT have partially acted must keep using Provider/Io/ NotFound so the use stays consumed. - The carrier refunds the consumed single use on `DidNotAct` (mirroring the `NoProvider` slice); every other Err keeps it consumed (fail-closed). Only the two provably-no-effect classes are refund-safe. - Wire the new variant through the two exhaustive matches: `Display` and the storage `From<ProviderError>` (→ 400-class InvalidPath). Test-first (mock providers, no production behavior change): - carrier_invoke_refunds_single_use_grant_on_did_not_act — a DidNotAct failure refunds, so the SAME single-use token reaches the provider twice. - carrier_invoke_keeps_single_use_consumed_on_acted_failure — an acted Provider failure keeps the use consumed (second call capability_denied). Real-provider migrations (e.g. read-only InspectProvider) are a tracked follow-up in KNOWN_GAPS with a per-op "provably no side effect" gate + a response-shape caveat — not a blanket sweep. Also records the G3b decision in KNOWN_GAPS: keep preview/enforce as DISJOINT tables (cross-checked), NOT a shared table (which would make the conformance test vacuous) — correcting a swarm proposal. Gates: cargo test (871 server lib + 33 provider green), clippy -D warnings, rustfmt --check, WCI alignment OK. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ider manifests Generalize the single rights fixture to a UNIVERSAL pin: enumerate every shipped provider manifest (22 with authority) and assert, for every declared op, that the verb-map-ENFORCED action (`required_action_for`) is a MEMBER of the manifest's PREVIEWED action set (`plan_provider_operation`). The two tables stay DISJOINT (no shared function), so drift fails loudly — Option B, not a shared table (which would make the test vacuous). Key correction during build: a naive `previewed == enforced` is wrong universally — a multi-action capability block legitimately previews a union, so the right invariant is membership (the manifest must declare AT LEAST the enforced action); a true drift is enforcement requiring an action the manifest never grants for the op (a verb-map Admin fallthrough). The pin surfaced 49 real drifts (all fail-CLOSED today: previewed-but-denied, never an escalation). A 4-agent classification swarm triaged them into a documented `known_divergences` ledger (read-safe / write / execute-egress / manifest-over-declares / HIGH-RISK-keep-Admin). The ledger is a ratchet: a NEW drift fails the test, and removing a fixed op without a real fix also fails (it cannot rot). Per-op RESOLUTION (verb-map entries for safe reads; restructure blanket-`execute` manifests like did/*; KEEP Admin for wallet-signing / key-release / decrypt / seal / broadcast) is tracked follow-up — deliberately NOT a bulk enforcement loosen. This CLOSES the G3b "drift can hide" gap. KNOWN_GAPS G3 row updated. Gates: cargo test (rights + universal conformance green), clippy -D warnings, rustfmt --check, WCI alignment OK. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ller refactor Verified the response-shape gate the KNOWN_GAPS caveat required before migrating InspectProvider to emit ProviderError::DidNotAct. Result: NOT a drop-in. Its send_raw Ok(error-status Value) contract is consumed by BOTH transports (the carrier AND the gateway provider proxy, gateway_provider_proxy.rs:1399) plus ~21 inspect-test assertion sites. Emitting Err(DidNotAct) would change the response shape on both transports and break those assertions — a scoped refactor, not a quick PoC. Recommendation recorded: the first real DidNotAct migration should target a carrier-only provider (no gateway dependency on its send_raw error shape), or do InspectProvider as its own chunk that updates the gateway proxy + tests. The DidNotAct mechanism itself stays proven by last turn's mock-provider PoC. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…cked ops
Shrink the G3b preview≠enforce ledger by 11, the swarm-verified subset where the
op was UNMAPPED (→ fail-closed Admin) but its shipped manifest ALREADY declares
the matching action — so adding the verb-map entry makes preview==enforce and
loosens enforcement only to the action the author already authorized (a pure
preview-honesty fix, no new authority).
Graydon-seat verification read each impl and confirmed the action class:
Read: download, events, metadata_index, read_bytes, reconstruct_listing,
export_graph, prepare_publish (pure assembly — "NEVER published")
Write: restore, write_bytes, import_graph
Delete: empty_trash (fs::remove_dir_all)
object-provider `share` deliberately STAYS in the ledger (grants access —
security-touching, held for a dedicated review). The ratchet did its job: it
flagged `prepare_publish` as healed-but-still-listed until I removed it, so the
ledger cannot rot.
Gates: cargo test (872 server lib, incl. universal conformance, green),
clippy -D warnings, rustfmt --check, WCI alignment OK.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…/ai/llama/tunnel)
Drain 12 more G3b drifts by making the manifests HONEST: the blanket
single-action capability blocks declared one action for ops the verb map enforces
at finer granularity, so preview≠enforce. Split each into per-action blocks that
match the (unchanged) verb-map enforcement — a preview-honesty change that grants
no new authority and drops no op (Miller-seat verified every op's action class).
- did-provider: [execute]* -> read{get_did,resolve,get_nickname,get_persona_did}
+ write{set_nickname} + execute{sign_chat_message,verify,verify_did_recovery}
- ai-provider: [execute]* -> read{list_backends,ping} + execute{chat_completions}
- llama-provider: [execute]* -> read{status,health,list_models} + execute{chat_completions}
(health/list_models verb-map Read added in ch1)
- tunnel-provider:[admin]* -> read{status,ping} + admin{start,stop}
Same resource/reason/audit_events preserved per manifest. The conformance ratchet
again caught healed-but-still-listed ops (did/*) until removed from the ledger.
Tracked separately (Miller): did `get_persona_did` now previews Read (matching the
EXISTING Read enforcement), but its impl create-on-first-call has Write semantics —
a pre-existing verb-map question, not a G3b drift.
G3b drain now 23/49 resolved; 26 sensitive-tail drifts remain (egress/actuator/
keys/signing — KEEP Admin).
Gates: cargo test (common 95 + runtime 345 + server 872, all green),
clippy -D warnings, rustfmt --check, WCI alignment OK.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
… splits
Drain 8 more G3b drifts. Two egress-security agents read the net-provider and
exit-provider implementations and confirmed: their egress ops (connect/stream/
http; quote/open_stream/close_stream/http_fetch) are genuinely Execute, and
NEITHER provider has any operation that mutates persistent state — so the manifest
`write` action was dead/over-declared.
- net-provider: [read,write]* -> read{status,resolve} + execute{connect,stream,http}
- exit-provider: [read,write]* -> read{status} + execute{quote,open_stream,close_stream,http_fetch}
(dead `write` dropped from both; enforcement unchanged — pure preview honesty)
- encrypt-provider: [write]{status,seal} -> read{status} + write{seal}
`status` drift fixed; `seal` STAYS in the ledger (HIGH-RISK: CEK sealing,
enforced Admin) for a dedicated review.
G3b drain now 31/49 resolved; 18 dangerous-tail drifts remain at Admin/tracked
(browser-actuator, drm open, encrypt seal, wallet signing/approval/secret-export,
key release, decrypt, chain broadcast, object share).
Gates: cargo test (common 95 + runtime 345 + server 872, all green),
clippy -D warnings, rustfmt --check, WCI alignment OK.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
Drain 5 more G3b drifts. A UI-actuation security agent read the
browser-engine-adapter implementation and confirmed: launch/attach_stream/
close_page/input/webrtc_signal are Execute (UI actuation — driving a headless
browser page), sandbox-scoped, with NO host command injection and NO secret
custody, so Execute (not Admin) is correct; status/page_status/screenshot/frame
are Reads; and NO op mutates persistent state (the manifest `write` was dead).
browser-engine-adapter: [read,write]* ->
read{status,page_status,screenshot,frame} +
execute{launch,attach_stream,close_page,input,webrtc_signal}
(dead `write` dropped; enforcement unchanged — pure preview honesty)
G3b drain now 36/49 resolved; 13 dangerous-tail drifts remain at Admin/tracked
(drm open, encrypt seal, wallet signing/approval/secret-export, key release,
decrypt, chain broadcast, object share).
Gates: cargo test (872 server lib green), clippy -D warnings, rustfmt --check,
WCI alignment OK.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…d teeth
W1b design contract (KVM-lane build): a per-capsule kernel firewall on the
capsule's TAP that physically drops egress outside its declared EgressReach,
fail-closed, with every blocked attempt written to the signed flight recorder.
- Keys on the now-honest egress capability (G3b drain: net/exit declare `execute`)
and the existing reach vocabulary (EgressReach::{None,Allowlisted,Open},
EgressAllowlist::permits, EgressReach::resolve).
- Threads the EXISTING TAP launch hook (supervisor.rs:1115, guest_network) — install
a per-TAP nftables chain (None=drop-all, Allowlisted=allow-resolved-only,
Open=allow+tagged-hot); tear down on reap (leak-free, mirrors BUG-2/3).
- Names the DNS problem honestly (host-scoped allowlist vs IP filter) with a
host-side DNS-proxy primary + IP-set interim.
- Spells out the privilege requirements (/dev/kvm + CAP_NET_ADMIN + nftables) — why
it is the KVM lane, not in-cloud — plus a 5-test plan incl. a compromised-guest
backstop.
The convergence: reach model + honest egress capability + flight recorder become
one enforced thing — packet-level proof the agent stayed in its lane (EU AI Act
containment). Alignment pins anchor the doc to the reach API + TAP hook it builds on.
Gates: WCI alignment OK (new W1b pins pass).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
Folds the two-seat council review of Sprint 35. Both seats returned
SHIP-WITH-FIXES; the central money invariant (no double-refund,
no refund-a-confirmed) already held. Two HIGH structural findings
converged on the idempotency guard's durability.
Red-team F1 (HIGH) + guardian F3 (HIGH) — the guard was blind if the
pending record failed to land AFTER an irreversible broadcast, and
money-bearing keys were evictable, both reopening the cross-window
double-buy. Fixed structurally:
- RECORD-BEFORE-BROADCAST: the pay closure now durably custodies the
key as Pending via PaymentLedger::begin_attempt BEFORE calling
provider.pay; if the ledger cannot custody it (per-capsule pending
cap, ledger full of money-bearing keys, persist failure) it REFUNDS
and DECLINES without broadcasting — money never moves into an
unrecordable state. After pay, finalize() transitions the placeholder
to the outcome. begin_attempt reopens a provably-nothing-moved
terminal for a legit retry and refuses a money-bearing key
(AlreadyActive).
- NEVER EVICT MONEY-BEARING KEYS: eviction now sheds only
NotCharged/ResolvedNotCharged (provably nothing moved); Pending/
Performed/ResolvedCharged are protected, so an eviction can never
forget a charged key. A cap full of money-bearing keys refuses new
inserts fail-closed.
Guardian F1/F2 + red-team F3 (HIGH/MED) — confirmation fail-closed:
tx_confirmation_live now gates BOTH verdicts on the depth floor (a
shallow revert is HELD, not refunded — a reorg could re-include it),
and an absent/empty/unparseable receipt status reads as HOLD not
success (a hostile receipt with a blockNumber but no status cannot
promote a buy). Extracted a pure classify_receipt() so these
money-critical rules are CI-testable without a live chain.
Wording/doc folds: guardian F3 (corrected the mis-stated "charged keys
never evicted" — now true by construction), F4 (added the ledger
snapshot back-compat test), F5 (FLINT doc no longer calls a DRM buy a
rail-trust attestation — it is chain-confirmed), F6 (reconcile doc
comment); red-team F2 (pending-quota availability regression),
F4 (best-effort receipt binding), F5 (drm:tx= discriminator) all stated
as tracked MKT-DRM residuals.
New ratchets: payment_ledger::tests::{money_bearing_keys_are_never_
evicted, begin_attempt_custodies_reopens_and_refuses, pre_s35_ledger_
snapshot_without_token_id_round_trips}; chain_tx::confirmation_tests::
classify_receipt_is_fail_closed_on_status_and_depth. Serialized the two
env-mutating build_pay_rail tests behind a shared lock.
Gate green: elastos-runtime 432, elastos-server 1230; clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Closes the primary open MKT-DRM residual: the cap was enforced in spend
units while the actual on-chain charge is the listing's pay-token
amount, so the meter bounded intent, not the money leaving the wallet.
The price gate. The DrmSettler trait gains quote() — a READ-ONLY price
source (buy_authority::quote_buy, no broadcast). The provider now:
resolve → quote → PRICE-GATE → settle. The gate converts the mandate's
cap to pay-token units via a declared mapping (amount ×
ELASTOS_DRM_SPEND_UNIT) and refuses BEFORE broadcast if it does not
cover the on-chain price (NotCharged/refund; fail-closed on an
unparseable price or a conversion overflow) — never buy above what the
mandate authorized. settle() binds the gated price as the buy's
expected price, so buy_access's own abort-on-drift fires if the live
price changed between quote and broadcast (closes the TOCTOU: the buy
can never settle above the gated price).
Declared unit mapping, fail-closed. ELASTOS_DRM_SPEND_UNIT is
REQUIRED on the live Chain rail (pay-token smallest-units per spend
unit, e.g. 1000000 for USDC) — build_pay_rail refuses to wire the DRM
rail without it rather than silently assume 1:1. Dev/chain-mock quote
free, so the gate is a no-op there.
Receipt honesty. rail_ref now carries ;price=<price>;tok=<pay_token>,
so verify-receipt shows the pay-token price actually authorized, not
just the spend-unit amount.
Ratchets: drm_marketplace::tests::{a_buy_below_the_on_chain_price_is_
refused_before_broadcast (settler never called), an_exact_match_buy_
proceeds_and_the_rail_ref_names_the_price (unit scaling + rail_ref),
an_unparseable_price_is_refused_before_broadcast};
server::tests::drm_rail_obeys_the_mock_money_discipline extended for
the missing-unit wire refusal. The S34/S35 e2e updated for the new
quote step + rail_ref format.
Gate green: elastos-runtime 432, elastos-server 1233; dev-modes lane
passes; clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Folds the two-seat council review of Sprint 36. Both seats returned
SHIP-WITH-FIXES; the money-invariant findings were the ship-blockers.
F1 (both seats — the buy above the cap): the gate compared the mandate
cap against the listing's PER-UNIT price, but buy_access charges
price × quantity, and the DRM BuyTarget left quantity unset — so an
ambient ELASTOS_DDRM_BUY_QUANTITY>1 env could settle N× the gated
price. Fixed: the DRM buy PINS quantity=1 (a pay-for-access buy is one
ACCESS_TOKEN; the per-unit gate is now the total charge, and no env can
inflate it).
F2 (both seats — pay-token flip): the buy bound only expected_price, so
ensure_no_drift's payToken check was a tautology (compared the settle-
time re-read against itself). A seller swapping the listing's pay-token
at an equal numeric price between quote and broadcast would settle in a
different, possibly far-more-valuable token. Fixed: quote_buy now
returns the RAW pay-token (not a "native" alias), and the DRM buy binds
expected_pay_token = quote.pay_token, so abort-on-drift fails closed on
a token flip too.
F3 (guardian — one unit maps one token): a single global spend_unit was
applied to whatever token a listing quoted (heterogeneous per-listing
tokens make one mapping unsatisfiable). Added a REQUIRED (live Chain)
ELASTOS_DRM_PAY_TOKEN: a listing quoting any other token is refused
before broadcast, so the declared unit always denominates the declared
token and the ceiling stays literal. build_pay_rail refuses to wire
the live rail without it.
Extracted ChainDrmMarketplace::buy_target so the money-critical target
invariants (quantity=1, price + pay-token drift armed, pinned
operative/tokenId) are CI-testable without a live chain.
New ratchets: drm_marketplace::tests::{the_drm_buy_target_pins_
quantity_and_arms_price_and_pay_token_drift, a_listing_in_a_different_
pay_token_than_declared_is_refused}; server::tests::drm_rail_obeys_the_
mock_money_discipline extended for the ELASTOS_DRM_PAY_TOKEN wire
refusal. Docs (FLINT row, rail runbook, KNOWN_GAPS) qualify the
"literal ceiling" claim with the operator-declared-mapping residual and
document the quantity pin + pay-token requirement.
Gate green: elastos-runtime 432, elastos-server 1235; dev-modes lane
passes; clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…lity Four-seat quality council (money spine, dispatch surface, receipt primitives, docs) over the whole Flint core. Every small-and-safe finding folded; structural refactors ledgered in docs/CODE_QUALITY_LEDGER.md. Money spine: - Pre-broadcast refusal sentinels are shared consts (buy_authority) now referenced by both the producers and the DRM refund classifier, so a rewording can never silently degrade a provable refund into a hold; pinned by a sentinel-anchor test. - Ledger admission (per-capsule pending cap + money-bearing-aware eviction) extracted to one admit_new_key home used by record and begin_attempt; a failed reopen now restores the WHOLE prior record (status, rail_note, refund_applied) with a ratchet test. - record/record_with_token re-documented as test-seeding only (the production path is begin_attempt/finalize). - ensure_budget post-publish persist failure now reports Poisoned (the honest variant), matching the file's own S29 F1 rule; ratchet added. - reconcile_drm_confirmations Confirmed/Reverted arms share one spine; quote/settle sold-out refusal is one const; parse_drm_tx doc + dead unwrap_or cleaned; contradictory test name fixed; RAIL_TIMEOUT named; in-flight slot guard simplified; guarded unwrap in tx_confirmation_live restructured to a match; buy_authority tests moved to tempfile. Receipt primitives: - The frozen-serialization invariant now lives on AuditEvent and ChainedRecord themselves (not just three fields): field order/names on EVERY variant are load-bearing for every signed chain. - One shared recompute_record_hash canonicalization recipe used by both verify_chain and verify_mandate_receipt. - signable_digest never silently hashes empty bytes on a serialization failure (expect over unwrap_or_default) and the SecureTimestamp JSON preimage coupling is documented + byte-pinned by a ratchet test. - responsible-entity syntactic bound has one home (capability::responsible_entity_syntax_ok) used by all three choke points; denial-reason test now exhaustive (WrongAgent/NoGrant pinned); silent pubkey-publish and corrupt-counter-file failures now log loudly; dead allow(unused_imports) removed. Dispatch surface: - drm_rail test now takes the crate-wide ddrm_env_lock (flaky-CI race with rights/mint authority tests closed) with documented lock order; build_pay_rail test guards ELASTOS_PAYMENT_RAIL; one shared EnvGuard. - RequestCapabilityOutput docs corrected to the actual wire statuses; trivial drm_state wrapper removed; ELASTOS_WEB_MAX_SPEND_CAP now warns on an unparseable override; chain_tx test module moved past the items that followed it. Docs / presentability: - Flint is now discoverable from the front door: README Documentation table + What Works Today, and docs/README.md deep reference. - FLINT_MANDATE_ENGINE.md: branch-anchored opener replaced with what Flint IS; the 1,900-word runtime.pay table cell exploded into a real "payment spine" section; mandate=standing-grant vocabulary stated; stale test counts and sprint-log tone removed. - DRM_MARKETPLACE_RAIL.md: DDRM_LEDGER annotated as not-a-typo, REQUIRED-in-practice vars marked, and a three-surface payment-state mapping table (rail/ledger/dispatch) added. - KNOWN_GAPS.md retitled runtime-wide; MKT-DRM residual list headed honestly; GLOSSARY gains the Flint vocabulary; CODE_QUALITY_LEDGER.md tracks the deferred structural cleanups (CQ-1..CQ-10). Gate: runtime 434 + server 1237 + common 103 green; dev-modes lane green; clippy --tests clean in the Flint core. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Closes KNOWN_GAPS MKT-DRM residual 2b: reconcile_drm_confirmations was correct but inert (operator/automation had to invoke it). The runtime now drives it unattended. The scheduler (spawn_drm_reconcile_scheduler, one per process, spawned next to the one rail build): - OFF BY DEFAULT: arms only when ELASTOS_DRM_RECONCILE_INTERVAL_SECS is set (u64 >= 1) AND the rail carries a DrmConfirmer (a new PayRail field, present only on the DRM rail — structurally un-armable on HTTP/mock). Malformed interval/batch env REFUSES to arm; an interval on a non-DRM rail warns and stays off. The arming decision is a pure fn (drm_reconcile_schedule_from_env), CI-pinned. - ZERO new money-moving code: a tick is the SAME reconcile_drm_confirmations pass the manual reconcile drives; every charge/refund still flows through reconcile_payment_core, exactly-once by the ledger's resolve semantics. - BOUNDED + ROTATING (council F1 fold): at most ELASTOS_DRM_RECONCILE_BATCH (default 64) entries per tick, overflow counted as `skipped`, and a rotating cursor starts each tick after the previous tick's last scanned seq (wrapping) — a stuck-unconfirmed prefix can never starve the entries (or refunds) behind it. - PANIC-ISOLATED per entry, with ledger-truth accounting (council F3 fold): a panicking confirmer/reconcile holds THAT entry and the tick continues; a panic after the durable resolution is accounted by what the ledger says and raises a money-state alarm, never a silent hold. - NON-WEDGING (council guardian F2 fold): at most one tick in flight — a hung chain RPC is skipped loudly each interval, never awaited into a dead schedule and never stacked into new blocked threads. Ticks run on the blocking pool; missed ticks delay (no catch-up bursts). - ATTESTED honestly (council F2 fold): a tick that SETTLED anything appends a best-effort signed drm_reconcile_tick event; idle and held-only ticks stay off the chain, so a stuck pending cannot grow the signed chain by one event per tick. Ratchets: a_tick_is_bounded_oldest_first_and_reports_what_it_skipped, a_stuck_oldest_entry_cannot_starve_the_entries_behind_it, a_panicking_confirmer_holds_that_entry_and_the_tick_continues, only_a_settling_tick_is_attested_held_and_idle_ticks_are_silent, the_drm_scheduler_arms_only_with_an_interval_and_a_drm_rail. Docs: DRM_MARKETPLACE_RAIL.md (env block + scheduler section + honest residuals), KNOWN_GAPS.md (MKT-DRM 2b CLOSED with stated residuals: opt-in by design; no subprocess read deadline yet; best-effort tick event), FLINT_MANDATE_ENGINE.md (payment-spine bullet). Gate: runtime 434 + server 1242 + common 103 green; dev-modes lane green; clippy clean. Council: two seats (principles guardian, red team), both SHIP-WITH-FIXES, all findings folded above with ratchets. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…tor watches
The first slice of the North-Star surface, shipped inside the existing
Mandates shell app. STRICTLY read-only: every pixel is a projection of
the one enforcing registry + meter + ledger (same Arcs as the pay gate);
the panel has no buy verb — operators grant, agents act through the
existing signed-intent dispatch.
GET /api/apps/mandates/marketplace (launch-token gated, 503 when pay is
unwired — the Money panel's posture):
- ASSETS: every DRM asset the ACTIVE pay-mandates scope (resource
elastos://runtime/pay/<asset>, method runtime.pay), with live on-chain
quotes via the existing read-only quote_buy path. Quote reads are
SINGLE-FLIGHT (council fold: a miss claims an in-flight sentinel under
the cache lock before any read starts, so "one live chain read per
asset per TTL window" is literal under any concurrency — the S33 XSS
actor cannot amplify browser fetches into a blocking-pool storm),
TTL-cached (30s, pruned on every pass, post-fetch stamped), and
fresh-read capped per view with cache hits FREE — coverage rotates
across refreshes instead of permanently starving the tail (council
fold, with a rotation ratchet).
- BUYS: the ledger's records with honest wording — a broadcast is
"awaiting chain confirmation", never a purchase (regression-tested
against "purchas*"); confirmed carries its tx. Every PENDING buy is
ALWAYS shown (council fold: a flood of newer settled entries can never
push a live obligation out of sight — ratcheted); the settled tail is
windowed with the window STATED (buys_total/buys_limit, "showing X of
N" in the panel). Outside the live Chain rights mode the panel says
quotes are free/synthetic rather than displaying fake prices.
- UI renders exclusively via textContent (asset ids, quote errors, rail
notes and tx are external bytes; tx display server-bounded).
The demo lane: `elastos mandate market-demo <asset> [--amount N]` — cap
→ single-asset pay-mandate bound to an ephemeral agent key → the agent's
signed buy through the existing dispatch → revoke + cap cleanup (a
grant whose token id cannot be read warns LOUDLY about the live
authority, mirroring `demo` — council fold). The buy's ledger record
remains for the panel to show.
New read-only accessors: PaymentLedger::{recent(limit), len, is_empty}.
Council: two seats (principles guardian, red team), both
SHIP-WITH-FIXES; no XSS / auth bypass / money movement found; all
findings folded above with ratchets
(quote_coverage_rotates_instead_of_starving_the_tail,
a_pending_buy_is_never_truncated_by_a_flood_of_settled_entries, +
marketplace_projects_pay_mandates_and_ledger_buys_with_honest_wording,
marketplace_route_requires_home_launch_token,
marketplace_without_a_rail_reads_unwired).
Docs: FLINT_MANDATE_ENGINE.md (Watch row + operator-surface section),
DRM_MARKETPLACE_RAIL.md (Watching it — the panel + the demo).
Gate: runtime 434 + server 1247 + common 103 green; dev-modes lane
green; clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
The loop's last structural gap closes: an agent could buy under a
mandate but not ask what things cost. runtime.market_quote is a READ
affordance behind the existing dispatch gate — quote the live terms of
a granted asset, and receive them in the dispatch response.
One quote spine (P5): a new crate::market_quote module owns the
single-flight, TTL-cached, read-only quote path (MarketQuoter trait;
LiveMarketQuoter = the existing quote_buy, no new chain code). The S38
Marketplace panel was REWIRED onto it and PayRail gained a shared
quote_cache Arc, so the panel and the affordance ride one identical
fan-out bound: at most one live chain read per asset per 30s window,
whoever asks, under any concurrency.
The affordance:
- MANDATE-SCOPED, no market-wide oracle: the resource is the same pay
resource the buy uses; the envelope gate's exact match confines
quoting to granted assets. One envelope carries ONE action, so quote
(read) and buy (execute) are TWO grants on one resource — the docs
and the demo now say and do exactly that (council guardian F1: the
demo originally issued one execute grant, so its quote was denied
wrong_action — dead on arrival; fixed with two grants, both revoked
at exit).
- EXPLICIT disclosure channel: IntentExecution::Performed gained
agent_visible_report — per-executor opt-in data surfaced as
DispatchIntentOutput.report ONLY on a Matched performance, bounded
and printable-filtered at the pipeline boundary (guardian F4,
mirroring rail_ref's emit-site discipline). market_quote discloses
the canonical terms; every other affordance passes None — state_get's
one-bit rule stays structural, pinned by an e2e negative control.
- HONEST reconciliation: discovery ("" declared) performs as declared;
attested mode echoes the ACTUAL terms so a changed listing reconciles
diverged, never a fabricated match — attesting the terms as of the
spine's last read (TTL caveat documented, guardian F3). A failed read
declines with the bounded error; dev/mock synthetic quotes are
documented as such (guardian F5).
- BOUNDED like money (council red-team F1): MAX_INFLIGHT_QUOTES=8 with
an RAII slot guard — K distinct assets against a hung RPC can no
longer park K blocking threads and starve the pay pipeline's pool;
over the bound refuses with retry, never queues. Chain-sourced terms
are held to the SAME bound as the agent's declaration before being
signed or disclosed (red-team F2 — validate what we sign); and the
cache prunes once per pass instead of per asset (red-team F3).
market-demo is now quote-THEN-buy: the agent reads the terms under its
read-mandate, decides, and only then dispatches the buy under its
execute-mandate — the first decision-making agent act in Flint.
Ratchets: market_quote_discovery_returns_terms_via_the_disclosure_channel,
market_quote_attested_mode_echoes_actual_terms,
market_quote_declines_on_read_failure_and_outside_the_pay_namespace,
market_quote_shares_the_single_flight_cache,
market_quote_refuses_malformed_terms_from_the_quote_source,
market_quote_inflight_reads_are_bounded_fail_closed,
an_agent_quotes_its_asset_and_state_get_still_discloses_nothing,
+ market_quote module unit tests.
Docs: FLINT_MANDATE_ENGINE.md affordance row (two-grants rule, TTL
caveat, dev-mode honesty), DRM_MARKETPLACE_RAIL.md (agent quoting +
demo).
Gate: runtime 434 + server 1256 + common 103 green; dev-modes lane
green; clippy clean. Council: two seats, both SHIP-WITH-FIXES, all
findings folded with ratchets.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
… chain
Closes the S37/S39-ledgered hung-read residual at its root. run_chain_capsule
(the ONE shared chain-provider subprocess helper — rights gate, resolve,
quote, buy prepare/broadcast, receipt/confirmation reads) now bounds the
whole conversation: ELASTOS_CHAIN_READ_DEADLINE_SECS (default 30; malformed
or <1 => the default, loudly), child spawned in its own process group, a
watchdog SIGKILLs the group at expiry, the child is always reaped.
The money line (both council seats verified SAFE at every consumer): a
deadline error carries CHAIN_DEADLINE_MARKER + "UNRESOLVED". On the SEND leg
it classifies INDETERMINATE (the tx may have broadcast — hold, reconcile),
NEVER a refund. On read legs it is an ordinary fail-closed refusal/hold.
Council: two seats, both SHIP-WITH-FIXES; all findings folded:
- pid/pgid-reuse kill window (red-team F1 / guardian F3): the watchdog is
disarmed and joined BEFORE the child is reaped, so it can only ever kill a
still-live, un-reaped child.
- unbounded response-line length (red-team F2): read_capsule_line is now
length-capped (MAX_CAPSULE_LINE = 4 MiB), so a firehose provider is a
bounded error, not an OOM.
- hostile-provider refund break (red-team F3, HIGH, pre-existing): a
`chain-provider op failed:` error is post-broadcast by construction, so
is_pre_broadcast_refusal now refuses to read any op-failed error as a
pre-broadcast refusal (=> Indeterminate/hold).
- error honesty (guardian F2): the deadline error keeps the underlying error
and `fired` is set only inside the unix kill.
- doc over-claim (guardian F1): the deadline bounds ONE chain conversation;
the wallet-provider sign leg and rights-provider decide leg are separate
undeadlined subprocesses — the tracked sibling residual. Plus the
availability-cliff note and comment-precision nits (F4/F5).
Ratchets: rights_authority::{a_hung_chain_provider_is_killed_at_the_deadline,
a_malformed_deadline_env_keeps_the_default_protection},
drm_marketplace::{a_chain_deadline_error_is_never_classified_as_a_refund,
a_post_broadcast_op_error_is_never_read_as_a_pre_broadcast_refusal}.
Gate: runtime 434 + server 1260 + common 103 green; dev-modes green; clippy
clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…unded reap
Factor Sprint 40's chain-conversation watchdog into ONE shared module
(api/capsule_watchdog.rs) and extend the deadline discipline to the two
remaining unbounded subprocess conversations a money/access path traverses:
- wallet-provider SIGN leg (WalletSession::exchange): a timeout is a
PRE-broadcast refusal — the tx was never signed, so provably NotCharged.
drm_marketplace::is_pre_broadcast_refusal recognizes the marker ⇒ refund,
the exact mirror of the chain send-leg (Indeterminate/hold) rule.
- rights-provider DECIDE leg (run_rights_capsule): a timeout DENIES access
(fail-closed — every access consumer denies on Err).
The shared module owns the subtle bits so no third copy can drift: the
process-group spawn, the disarm-before-reap ordering (a recycled pid can
never take a stray group kill), the length-capped read, and the shared
deadline knob.
Council fold (principles-guardian + red-team, both SHIP-WITH-FIXES):
- guardian F1 (HIGH) + F2: the disarm-before-reap left the REAP itself
unbounded — a provider that answered every op then refused to exit on
EOF/shutdown would park the runtime thread forever on child.wait(), the
very hang the deadline exists to end, slipped past the read into the reap.
New reap_grouped() gives a brief grace then group-SIGKILLs an un-exiting
child; wired at all three sites (chain, rights, wallet shutdown).
- guardian F3 (P12): scoped the "every money/access boundary is bounded"
doc claim to the three provider conversations actually bounded, restored
the unix-only qualifier, and registered the access-path sidecar residual.
- guardian F4: added the live-kill ratchet the money-critical sign leg
lacked (a_hung_wallet_provider_is_killed_and_classified_pre_broadcast),
asserting bounded return + marker + end-to-end refund classification.
- guardian F5: a watchdog fire now poisons the session so no later leg runs
on the dead child (a fired-but-read-Ok race can no longer degrade a
provable refund to a stranded Indeterminate hold); deadline errors append
the underlying read error for diagnostics.
- guardian F6 / red-team F1: honesty comments — the marker names the
session's purpose (any pre-broadcast leg), the wallet read is now capped,
and a footgun note that a future human-consent approve must run OUTSIDE
this deadline.
- red-team F2: DeadlineWatchdog now disarms on Drop, so an un-disarmed
watchdog is a no-op rather than a latent full-deadline group-kill.
Gate: server lib 1263 (default) / 1264 (dev-modes), runtime 434, common 96 —
all green; every new ratchet returns bounded (~1s, not the stub's 300s).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ng open/view
Close the last residual the Sprint 41 guardian left open: the access-path
sidecar helper reads (the media/object content authorities and the trustless
grant sidecar) were still unbounded, so a hung or hostile content-authority
subprocess could park a request thread forever. This wires those reads onto
the EXISTING shared capsule_watchdog module (no fourth copy of the ordering)
and adds the shared pieces they need:
- ReapGuard: a spawned child that is BOUNDED-reaped on drop unless disarmed,
so EVERY early return between spawn and the point the child is handed to
its owner reaps it (no leaked live process, no zombie).
- read_line_deadlined / read_open_deadlined: per-op VIEW reads use the short
shared deadline; content-OPEN descriptor reads use a new, longer
content_open_deadline (ELASTOS_CONTENT_OPEN_DEADLINE_SECS, default 120)
because a media open runs ffmpeg + a seal handshake before it answers.
- read_to_eof_deadlined: the ONE bounded one-shot read (the grant sidecar).
- ACCESS_DEADLINE_MARKER: a content-authority timeout is a DENY (fail-closed)
— the mirror of the rights-decide rule, NEVER a money decision (these sit
on the open/view paths, not the pay spine).
Applied to object_authority (both descriptor reads + the object/page VIEW
reads + grouped spawn + bounded Drop), media_authority (both descriptor reads
+ the segment VIEW read + grouped spawn + bounded Drop, its local reaper
replaced by the shared ReapGuard), and access_grant (the one-shot run_sidecar).
Council fold (principles-guardian + red-team, both SHIP-WITH-FIXES):
- RT-F1 (HIGH) / guardian-1: object_authority leaked a LIVE child on a
well-formed-but-incomplete descriptor (field-extraction `?` after a good
read). Closed by the shared ReapGuard — every early return now reaps.
- RT-F2 (MED-HIGH): access_grant leaked the child on the stdin-write (EPIPE)
path — the reap ran only after the read. Closed by the same guard.
- RT-F3 (MEDIUM): the RPC-tuned 30s deadline silently capped an ffmpeg open
(an availability cliff). Split into the separate content_open_deadline.
- RT-F4 / guardian-2: child_pid() returned 0 → a fire would kill(-0) the
gateway's OWN process group. ReapGuard::pid panics instead, and arm now
refuses to kill on pid 0.
- guardian-3: access_grant hand-rolled a fourth copy of the arm/read/disarm/
reap ordering. Factored into the shared read_to_eof_deadlined (which also
now carries the marker, closing the doc over-claim).
- guardian-4: reap_grouped returns the ExitStatus so the one-shot folds a
non-success exit into its error.
- guardian-5/6/7/8: fmt fix; a hung-VIEW-read ratchet (the per-op leg,
distinct from the open); the write-leg-is-sub-pipe-buffer note; the stale
reap-backstop comment corrected; the prepare/assemble spawn_blocking
follow-up registered in KNOWN_GAPS.
Ratchets (all bounded ~1-2s vs a 300s stub): a hung media/object authority and
a hung grant sidecar are each killed and DENIED, and a sidecar that answers the
descriptor then hangs on a VIEW read is separately bounded.
Docs: the S41 sidecar residual is now CLOSED in KNOWN_GAPS, DRM_MARKETPLACE_RAIL
and FLINT_MANDATE_ENGINE — every content-open/view + provider conversation the
pay/access pipeline traverses is bounded, reap included, kill unix-only.
Gate: server lib 1267 (default) / 1268 (dev-modes), runtime 434, common 96 —
all green; fmt + clippy clean on the touched files.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…lassifier
The last soft spot in the money classifier: `drm_marketplace::is_pre_broadcast_refusal`
decided refund-vs-hold by matching SUBSTRINGS of `buy_access`'s error string —
provider-controlled text touching a money decision. This makes the decision a
TYPE, not a string.
- `buy_access` now returns `Result<BuyOutcome, BuyError>` where
`BuyError::{PreBroadcast, Indeterminate}` is decided BY CONSTRUCTION: each
error site is mapped by its position relative to the broadcast op. Every
`broadcast_signed_*`/`broadcast_mock` call and every post-broadcast op is
`.map_err(Indeterminate)`; every pre-send leg (wallet-not-linked, source,
sold-out, drift, the wallet SIGN leg, the chain PREPARE read, the missing-
signature precondition, the dev record) is `.map_err(PreBroadcast)`.
- `DrmSettleError::from_buy_error` maps the variant. The string classifier
(`is_pre_broadcast_refusal` + its `for_test` bridge + the pre-broadcast
sentinel list + the `chain-provider op failed` exclusion) is DELETED.
- There is NO `From<String> for BuyError`, so a bare `?` on a broadcast call
will not compile — a future refactor cannot silently mis-map a send error.
- Side benefit the type unlocks: a chain deadline on the PREPARE (read) leg is
now correctly refundable while the same marker on the SEND leg stays held —
a distinction the string could not draw (all deltas are safe-or-refund).
Council fold (principles-guardian SHIP-WITH-FIXES + red-team SHIP-CLEAN; the
red team confirmed the invariant — no provider input can refund a possibly-sent
tx — holds by construction):
- guardian F1 (must-fix, P12): rewrote three wallet_signer doc/test comments
that still pointed at the deleted classifier — the wallet marker is now
diagnostic text, classification is the call-site variant.
- guardian F2 (P12 honesty): the prepare-leg refinement is transitively
covered by the sign-leg call-site mapping; reworded KNOWN_GAPS so "Proven
by" no longer adjoins an unratcheted claim, and registered the dedicated
prepare-leg fixture as a follow-up.
- guardian F3: added ratchets for the two previously-untested variant sites —
a dev record failure types PreBroadcast (a behavior change the typing makes
correct) and a post-broadcast record failure types Indeterminate.
- guardian F4 (P16): documented the trusted-binary caveat on the PreBroadcast
variant (the runtime owns the only broadcast; env allowlist is the follow-up).
- red-team INFO-2: removed the dead `needs a signed transaction` phrase check.
Retired ratchet intents preserved and strengthened: the hostile-provider proof
is now `from_buy_error_classifies_by_variant_not_by_string` (an Indeterminate
error embedding EVERY sentinel + both deadline markers still holds) plus the
end-to-end `a_broadcast_op_error_types_the_buy_as_indeterminate_even_with_a_sentinel`.
Gate: server lib 1264 (default) / 1269 (dev-modes), runtime 434, common 96 —
all green; fmt + clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…RM 2d
The DRM confirmation reconciler used to identify its pending records by
sniffing `rail_note` for a `drm:tx=` prefix, so a hostile HTTP payment
endpoint whose Indeterminate body began `drm:tx=` could get its pending
polled/resolved by the DRM driver (council S35 red-team F5, fail-closed
direction but wrong). This replaces the note-sniff with a STRUCTURED tag.
- `PaymentRail::{Unknown, Http, Drm}` on `PaymentRecord`, stamped from the
paying provider at `begin_attempt` via a new REQUIRED `PaymentProvider::
rail()` method (no default — the tag is now a COMPILE-TIME property, the
S43 type-over-comment lesson). `DrmMarketplaceProvider::rail()==Drm`,
`HttpPaymentProvider::rail()==Http`.
- `reconcile_drm_confirmations` selects via `is_drm_pending`: Drm→ours,
Http→never (regardless of note), Unknown→bounded pre-S44 note fallback.
A positively-tagged Http pending — even one whose body a hostile endpoint
crafted to begin `drm:tx=` — is never polled by the DRM driver.
- Back-compat: `#[serde(default)]` ⇒ pre-S44 snapshots load as `Unknown` and
keep the note fallback so in-flight pendings reconcile across the upgrade;
a re-persisted legacy record gains the tag.
Plus docs/LIVE_BUY_RUNBOOK.md — the operator-driven procedure for a real
testnet buy (Grant→Act→Prove); deliberately OUT of the CI gate (the sandbox
has no funded wallet/network — the gate proves everything else via mocks).
Council fold (principles-guardian SHIP-WITH-FIXES + red-team SHIP-CLEAN; the
red team confirmed both money directions — no hostile HTTP pending is
DRM-resolvable, and no genuine DRM pending is stranded by mis-tagging, since a
money-bearing pending is un-reopenable so its tag is immutable):
- guardian F1 (ship-blocking, P12): fixed a KNOWN_GAPS self-contradiction
(the residual index said 2d open while the cell marked it CLOSED) + the
stale row title.
- guardian F2 / red-team F1: made `rail()` a REQUIRED trait method (removed
the default) so "every new record is positively tagged" holds by
construction, not by a doc comment — 7 impls updated.
- guardian F3 / red-team F5: ratcheted the reopen-adopts-rail behavior
(and that a money-bearing pending's tag is immutable ⇒ no stranding) +
an e2e assertion that the production pay closure stamps `Drm`.
- guardian F4 / red-team F2: a Drm-tagged pending with no `drm:tx=` note is
now visible (a one-time WARN) instead of silently folded into the
never-mining set; ratcheted (confirmer never called, money unchanged).
- red-team F4: documented the forward-incompatible-downgrade constraint on
`PaymentRail` (unknown rail string fails load fail-closed, no serde(other)).
- guardian F5/F6: qualified the S35 round-trip comment and marked
`seed_pendings` as deliberately exercising the legacy fallback.
Gate: server lib 1268 (default) / 1273 (dev-modes), runtime 434, common 96 —
all green; clippy clean on the touched regions.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…chain scaffold
The live-money last mile, in two honest halves. The sandbox has no funded
wallet or network, so a real on-chain buy CANNOT run here — and nothing in
this change claims one did. What ships:
(A) GATE-RUNNABLE (runs every push) — two tests in `verify_receipt_cmd` prove a
receipt carrying a DRM settlement `rail_ref` in its signed `CapabilityUse`
is an admissible artifact through the ACTUAL standalone `verify-receipt`
CLI evaluator: written to disk as JSON, read back, verified AUTHENTIC
(exit 0) with the pinned signer, the `drm:tx=` reference surviving the
round-trip; and a byte-tampered settlement tx breaks the record-hash
recompute ⇒ INVALID (exit 1). This is the auditor's artifact path, beyond
the in-memory `verify_mandate_receipt` the S35 e2e already covers.
(B) NETWORK-GATED (operator/CI only) — a new `live-chain` cargo feature gates
`tests/live_drm_buy.rs`, an `#[ignore]`d integration test that drives the
real `ChainDrmMarketplace` (resolve → on-chain price gate → sign → broadcast
→ confirm) per docs/LIVE_BUY_RUNBOOK.md. Triple-fenced from the gate: the
whole file is `#![cfg(feature = "live-chain")]`, the fn is `#[ignore]`d, and
CI never enables the feature. It asserts the buy is HELD at broadcast
(`Indeterminate` with a real tx, never charged — the S35 invariant) then
confirms on-chain.
Council fold (both seats SHIP-WITH-FIXES; both verified by execution that the
gate tests are load-bearing, the live test cannot leak into the gate — proven
three ways — and NOTHING claims a live buy executed):
- red-team F1: aligned the gate fixture's emit shape to the real reconciler
(`Action::Execute` on `elastos://runtime/pay/<payee>`) and softened the
"exact shape" wording so the fixture doesn't over-claim production fidelity.
- both F2: reworded the live test's doc from "the whole stack / never refund
a sent tx" to the honest provider→chain leg it actually drives; the
ledger/reconcile/receipt legs are gate-proven against mocks + driven live by
the gateway per runbook §2.
- red-team F3: added a network-free CI `cargo check --features live-chain
--tests` step so a rename can't silently rot the operator's only live tool.
- guardian F1: assert `ELASTOS_DDRM_BUY_SIGN=wallet` up front (equally
mandatory with the empty subject) so a misconfig fails with the true cause.
- red-team F4: assert the rail_ref carries a real 0x+64-hex EVM tx before
polling, so a stub binary can't green the confirm loop on garbage.
- red-team F5: pin the tamper failure to `!hashes_ok` (not merely
`!structurally_valid`) — the mechanism is now a ratchet.
- guardian F3 / red-team F7: documented that a live re-run RE-SPENDS (settle
ignores the idempotency key; the standalone test bypasses ledger dedup) in
the runbook + the timeout assert, and that a `live-chain` build (pulls in
dev-modes) must never be reused as a release artifact.
- guardian F6: KNOWN_GAPS + runbook now state plainly this has NOT yet been
run against a live testnet in this repo (record the first run's tx hash).
Gate: server lib 1268 (default) / 1273 (dev-modes), bin 105 (+2 CLI ratchets),
runtime 434, common 96 — all green; live-chain test compiles + is correctly
ignored (0 run in the gate).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ic delta) CI declares `cargo fmt --all -- --check` but the tree had drifted (344 diff sites across 24 files) — a gate that cannot pass is a lie the honesty regime cannot carry (S46 council Track B4). This is `cargo fmt --all` verbatim: purely mechanical reflow, zero semantic change, proven by the unchanged test counts on the follow-up commit's full gate. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…leg ratchet, spawn_blocking)
Three council carry-overs closed, continuing the drive to 10/10 while the live
Base run waits for the operator:
- B1 (P16, council S43 guardian F4 partial): `capsule_watchdog::spawn_grouped`
— the single seam every provider/sidecar spawn goes through — now strips
RUNTIME_ONLY_SECRETS from the child env: ELASTOS_DDRM_BUY_SIGNED_TX (a
fully broadcastable signed tx; leaked to a hostile provider binary it could
be broadcast out-of-band while the read leg fails "pre-broadcast") and
ELASTOS_PAYMENT_TOKEN (the HTTP rail bearer). Both are consumed exclusively
in-process and passed onward as explicit op arguments, so no capsule has a
legitimate use for the env copy. Ratchet:
`runtime_only_secrets_are_stripped_from_spawned_capsules` (canary secrets
ABSENT in the child; ordinary ELASTOS_* config still passes through).
Honest scope: a targeted denylist of runtime-only secrets — the full
per-capsule env allowlist remains the stronger tracked hardening.
- B2 (closes council S43 guardian F2): the dedicated PREPARE-leg ratchet
`buy_authority::a_chain_prepare_deadline_types_the_buy_as_pre_broadcast` —
a chain stub answers the LISTING read then hangs only on
`prepare_transaction`; the buy is killed at the deadline and typed
`BuyError::PreBroadcast` (refund), carrying the SAME CHAIN_DEADLINE_MARKER
the SEND leg holds as Indeterminate — the call site decides, not the bytes.
Guards the ordering invariant a refactor could silently break (hoisting the
prepare out of the sign closure).
- B3 (closes council S42 guardian F8): `access_grant::prepare`/`assemble` in
viewer_open now run inside `tokio::task::spawn_blocking` — the
deadline-bounded (≤~31s) sidecar wait holds a blocking-pool thread, never
an async worker; a panicked task maps to the same fail-closed error arm.
- KNOWN_GAPS updated for all three closures (F4 marked PARTIALLY closed —
the allowlist stays tracked).
(The preceding commit 133c340 is the mechanical repo-wide `cargo fmt` that
makes CI's declared fmt gate truthful — B4.)
Gate: server lib 1269 (default) / 1275 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift; clippy clean on touched files.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ard + gate the dev-modes lane
The S46 council (red-team SHIP-WITH-FIXES + guardian SHIP-WITH-FIXES) proved
three of the sprint's claims outran the code; this folds all of them:
- red-team F1 / guardian F1 (HIGH, the blocker): "single spawn seam" was
FALSE — `ProviderBridge::spawn` (every general provider capsule), the
carrier service spawn, and the shell-capsule spawns (main/serve_cmd)
bypassed `spawn_grouped` and inherited the rail bearer under a normal
HTTP-rail config. The strip now lives as ONE shared list
(`elastos_runtime::provider::RUNTIME_ONLY_SECRETS`) applied at ALL FIVE
capsule seams; host-tool spawns (git/tar/self re-exec, which legitimately
keeps its env) are deliberately not stripped.
- red-team F5 (MEDIUM): the canary proved the helper strips, not that spawns
route through it. Added the SOURCE-STRUCTURAL guard
`every_command_spawn_site_is_a_known_seam_and_capsule_seams_strip_secrets`:
every `Command::new` site in elastos-server/elastos-runtime must be a
classified capsule-seam-carrying-the-strip or a consciously allowlisted
host tool — a NEW spawn path fails the gate until classified.
- guardian F2 (MAJOR, P12): `assemble_cached` (the popup-free re-open path)
still shelled the grant sidecar on the async executor one line below the
fix — now also in spawn_blocking, preserving its fall-back-to-enrolled arm.
- guardian F3 (MAJOR, gate honesty): the dev-modes construction ratchets
(S43 typed BuyError, the S46 prepare-leg deadline) were NEVER COMPILED by
the gate. `just _verify-tail` and ci.yml now both run the
`--features dev-modes` lib lane — a ratchet outside the gate cannot ratchet.
- guardian F4 (MINOR, accepted): the new test's env hygiene matches its S43
siblings' pattern; adopting the RAII EnvGuard across the ddrm test family
is noted follow-up work, not folded here.
- Both seats verified the fmt commit (133c340) mechanically reproducible
bit-for-bit, B2 (prepare-leg ratchet) rigorous, and B3 semantics preserved.
- KNOWN_GAPS updated to the now-true wording (all seams named, the false
first-cut claim recorded, the structural guard cited).
Gate: server lib 1270 (default) / 1276 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ps (Track C1)
The media/object viewer session stores were unbounded HashMaps: an entry —
holding MB-scale init bytes, sealed material, and an Arc to a live
KEY-AUTHORITY SUBPROCESS — lived until an explicit client close. Sustained
opens grew memory and process count without limit; an abandoned tab pinned a
child process forever. Worse (S47 guardian F2): /init, /cover, and the
manifests had NO expiry check, so an expired session's bytes were still
served (only segment/bytes/page reads failed closed).
New `api/session_bounds.rs` — ONE discipline both stores apply:
- SWEEP: every admission and every lookup removes all EXPIRED entries.
- CAP: at MAX_VIEWER_SESSIONS (256/store) evict soonest-to-expire live
sessions (TTL order, not LRU — fixed-window sessions, no per-read
bookkeeping, ungameable by touching your own sessions) with a warn.
- DEFERRED-DROP CONTRACT (the council crux, both seats HIGH/MEDIUM): a
session drop can reap a subprocess with ~1s grace; the sweep runs under
the store Mutex, so dropping in place would stall EVERY viewer's lookup
for N×1s on a mass sweep (worst case ~4 min with 255 hung authorities).
The helpers RETURN removed values; call sites (put/get/remove, both
stores) drop them after releasing the lock.
- Fail-closed: an evicted session's next read is "no such session" — the
viewer re-opens through the FULL authorization gate. Eviction can never
grant access (red-team confirmed; in-flight reads finish on their own
Arc clone).
Council fold (both seats SHIP-WITH-FIXES; the blocker was pre-folded):
- F1 (HIGH): deferred off-lock drops — including the pre-existing
remove_*_session single-drop.
- G-F2 (P12): module docs now credit the lookup-sweep with closing the
previously-unchecked /init//cover/manifest expiry (the change undersold
itself).
- RT-F2/G-F3: the process-global, principal-blind cap + the authorized-
opener precondition + per-principal fairness as future work — documented.
- RT-F6/G-F4: honest worst-case math on the cap (≈2.3 GB + ~512 procs/store
fully adversarial, resting on MAX_CAPSULE_LINE) replaces "the host
survives".
- G-F5a: the wiring test now inserts the expired session LAST, so the
LOOKUP sweep is provably the remover.
- G-F5b: the object store's missing wiring test recorded as an honest gap
(non-optional Arc<ObjectAuthorityProc> holds a real Child; wiring is
byte-identical to the tested media store).
- Clock/concurrency/off-by-one all verified consistent by the red team
(sweep and route guards share the same predicate; no serve-after-sweep
window; a backward clock step keeps MORE, never mass-sweeps).
Gate: server lib 1275 (default) / 1281 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…er contract (Track D1+D2)
The North Star breadth move: prove the market-provider seam is not DRM-shaped
by shipping a second chain-settled vertical behind it, and publish the
contract a third party could implement a provider from.
D1 — docs/SPEC-market-provider-v1.md: the versioned contract. The runtime owns
every money DECISION (mandate gate, meter, record-before-broadcast ledger,
receipts, reconciliation); a provider owns only the money REPORT (the
two-generals pay() classification + the compiler-required rail() tag + the
fail-safe confirmation reader). A conformance checklist maps each obligation
to its mechanical enforcement.
D2 — api/erc20_checkout.rs (`Erc20CheckoutProvider`): `runtime.pay
{payee: 0x…, amount}` becomes `transfer(payee, amount × ELASTOS_ERC20_SPEND_UNIT)`
on ONE operator-declared token:
- Typed by construction (the S43 discipline): bad address / unit overflow /
calldata / the wallet SIGN leg (incl. the chain PREPARE read) are
NotCharged — provably nothing moved; the broadcast op and after are
Indeterminate carrying `erc20:tx=<hash>;to=…;amount=…;tok=…`
(delimiter-stripped per component). A broadcast-accepted checkout is NEVER
charged until confirmed — Ok-at-broadcast does not exist.
- `rail() = PaymentRail::Erc20` (new variant; snake_case "erc20";
forward-compat posture per the S44 contract — a coordinated upgrade).
- Mock settlement: dev-modes + the explicit mock-money opt-in. Live: wallet
managed signing (dev-modes; a release build refuses NotCharged — the
external-signature checkout flow is a stated follow-up, same posture as
the DRM rail).
The chain-settled reconciler, generalized rail-STRICTLY:
- `parse_chain_tx(rail, note)`: Drm parses only `drm:tx=`, Erc20 only
`erc20:tx=` (a cross-rail note is a tx-less orphan — left Pending, warned,
never polled); the pre-S44 `Unknown` legacy fallback stays DRM-ONLY (it
never widens to new rails); Http never parses.
- `is_drm_pending` → `is_chain_settled_pending` ({Drm, Erc20} = ours);
ONE shared `confirm_chain_tx` (receipt + depth floor) both verticals use;
`Erc20CheckoutProvider` impls the confirmer via it, so the in-runtime
scheduler polls checkout pendings exactly like DRM buys.
Wiring — `ELASTOS_PAYMENT_RAIL=erc20` mirrors the DRM arm's fail-closed
discipline: ELASTOS_ERC20_TOKEN + ELASTOS_ERC20_SPEND_UNIT required (the cap
is a literal on-chain ceiling, never a silent 1-wei assumption); durable
meter/ledger required; mock mode refuses without ELASTOS_ALLOW_MOCK_PAYMENTS.
7 new ratchets: transfer-calldata words; junk payees refused pre-money;
unit-overflow refused pre-money; broadcast held-Indeterminate with a
parseable ref (e2e, dev-modes); Erc20 pendings reconciled + the S44 walls
hold for the new rail (hostile Http `erc20:tx=` never polled; Unknown legacy
stays DRM-only); parse_chain_tx rail-strict; the "erc20" serde round-trip.
Gate: server lib 1281 (default) / 1288 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift. Council review in flight; findings fold
as a follow-up commit.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ot warn
Council S48 red-team returned SHIP-CLEAN (all five money invariants confirmed
by construction: no charge-at-broadcast, no refund-after-send, the cap is a
literal ceiling, cross-rail steering impossible, the payee cannot forge
calldata). Two LOW footguns folded:
- LOW-2: ELASTOS_ERC20_MODE parse is now case-insensitive AND an
unrecognized non-empty value REFUSES TO WIRE (fail-closed) rather than
silently defaulting to Live — a typo'd mode must be visible, never a
surprise settlement mode. Empty/absent still defaults Live (the stricter
path).
- LOW-1: a boot-time warn when mock mode wires — mock settlements produce
tx hashes the LIVE confirmer can never find, so mock pendings hold their
reservations until manually resolved (money-safe over-hold, stated).
INFO-1 (the wallet-capsule trust posture on the live sign leg) is inherited
from the DRM rail unchanged and already documented there.
Gate: check clean, fmt 0 drift, server lib default 1281 green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…r + wiring ratchet
Council S48 guardian returned SHIP-WITH-FIXES (the money core verified clean —
every leg typed by construction, no Ok path exists, the S44 walls hold and are
re-ratcheted for the new rail). All seven findings folded:
- F1 (HIGH, gate break): erc20_checkout's dev-modes-shadowed code left 5
clippy warnings in the default build, breaking the declared `-D warnings`
gate. Fixed with cfg-gated imports/helpers + a documented release-shape
dead_code allow. And went further (the S46 gate-truth rule): the WHOLE
workspace is now clippy-clean at 0 warnings — folded the pre-existing
drift too (wasm.rs too_many_arguments documented allow; session_bounds
deliberately-write-only field; parse_chain_tx lifetime elision;
intent_executor + bench doc-list indentation; market_quote's unit error
is now the named `ReadInFlight` type; carrier's 3 test env-lock guards
held across await get a justified allow — the lint targets production
deadlock risk, not test env serialization).
- F2 (MEDIUM, P11+P12): the rail selector is now CLOSED-WORLD — an
unrecognized non-empty ELASTOS_PAYMENT_RAIL refuses to wire (fail-closed)
instead of silently falling through to the HTTP/mock arms; the SPEC §6
"never degrades silently" sentence is now true of the selector itself.
- F3 (MEDIUM, P12): `erc20_rail_obeys_the_wiring_discipline` — the missing
wiring ratchet mirroring the DRM sibling: typo'd rail refuses; no token /
no unit / malformed unit / unrecognized mode / mock-without-opt-in all
refuse; fully-declared wires durable with the Erc20 tag + the confirmer.
(Takes the documented two-lock order — the first run caught an env race.)
- F4: SPEC row 4 reworded honestly (provider broadcast ratchet + the
shared-spine e2e via DRM, not a uniform "e2e").
- F5: the constructor's spend_unit clamp is now debug_assert'd + documented
(never silent).
- F6 (P12): wiring Live mode on a release build now warns at boot that
every checkout refuses until the external-signature flow ships.
- F7: SPEC §6 names the shared chain-config envs (the DDRM-era names; a
rail-neutral rename is tracked); the scheduler's "not the DRM rail"
doc/warn updated to "not a chain-settled rail".
Gate: server lib 1282 (default) / 1289 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift; cargo clippy --workspace --all-targets:
ZERO warnings (the -D warnings gate passes truthfully).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…d by a conformance ratchet (Track D4)
docs/SPEC-mandate-v1.md: the byte-level formats behind Grant → Act → Prove,
written FROM the code so an independent party can implement verification with
no Flint code:
- §1 the trust model, stated without inflation (self-asserted signer — an
unpinned verification is NOT a trust decision; tamper-evident not
tamper-proof; the two completeness bounds).
- §2 the chained record + the exact hash preimage
(domain ‖ seq_be8 ‖ prev_hash32 ‖ event_json) + sig over the 32 RAW hash
bytes; the one canonicalization recipe (re-serialize the deserialized
event).
- §3 the receipt document; §4 the set-binding message byte layout (scope tag
byte, BE length prefixes, record hashes as 64-char ASCII hex).
- §5 the mandate events (INTERNALLY tagged via "type"; absent-when-unset
rules that keep old chains verifying; rail_ref formats cross-referenced to
SPEC-market-provider-v1).
- §6 the verification algorithm step by step + every verdict bit + the
fail-closed exit-code contract (0/1/3/4).
- §7 the signed intent + its preimage (domain WITH trailing NUL,
LITTLE-endian length prefixes — the endianness difference vs §2/§4 is
called out), and what the runtime enforces before executing.
- §8 versioning: frozen shapes, append-only optional fields, new tag for any
break.
The conformance ratchet `audit::tests::the_wire_format_matches_spec_mandate_v1`
pins the schema tag, both domain strings, every serialized key of the
document/record/scope/events, the timestamp shape, and that an exported
receipt verifies AUTHENTIC under §6. Writing it caught two real spec errors
before review (the event enum is internally tagged, and SecureTimestamp is
{unix_secs, monotonic_seq} — not the shapes first drafted), which is exactly
the failure mode the ratchet exists to prevent.
Gate: runtime 435 (+1), server 1282/1289, bin 105, common 96 — all green;
fmt 0 drift; workspace clippy 0 warnings. Council review in flight; findings
fold as a follow-up commit.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…hets
Council: guardian SHIP-WITH-FIXES (3 HIGH P12 defects), red-team
SHIP-WITH-FIXES (the crypto core sound — 3 domains separated, scope-tag byte
defeats binding replay, no path lets a self-signed forgery reach exit 0 —
but the spec under-specified canonicalization and had two over-claims). All
folded, some in CODE:
- guardian F1 (HIGH): §5 listed a phantom `capsule_id` on capability_revoke;
the real event is exactly {type, timestamp, token_id, reason}. Fixed the
spec AND added a byte-identity fixture for revoke (and grant) to the
conformance ratchet — revoke was the one §5 event the ratchet hadn't pinned,
which is exactly how the error slipped in.
- guardian F2 (HIGH) / red-team domain check: §7 cited a ratchet that only
pinned the timestamp segment. Added the known-answer ratchet
`the_intent_preimage_digest_is_frozen` — the WHOLE preimage (trailing-NUL
domain, field order, little-endian prefixes) as one fixed digest — and the
spec now cites it truthfully.
- guardian F3 (HIGH): event byte-ORDER was neither specified nor pinned. §5
now gives byte-exact compact-JSON templates (type first, declared order,
escaping rules) and the ratchet pins grant+revoke bytes.
- red-team F3 (MEDIUM, CODE): the receipt verifier used non-strict ed25519
while the intent path uses verify_strict — two conforming verifiers could
disagree on a malleated signature. Switched both receipt sig checks to
verify_strict; the spec's Primitives now mandates strict verification.
- red-team F1/F2/F4/F5/F7 + guardian F4/F6/F9: canonicalization caveat on the
"no Flint code" claim; the contiguous-end-truncation exception stated in §1;
"a PRESENT set_binding MUST verify for both scopes" (the dangerous
accept-a-tamper reading closed); "verify over the RECOMPUTED hash, never the
claimed"; §7 qualified (agent-key binding only when bound; intent_id replay
key vs the flint-<sig> payment key); grant-position-free + set_binding null
accepted; lowercase-hex + length-not-the-separator domain notes.
Gate: runtime 436 (+1 KAT), server 1282/1289, bin 105, common 96 — all green;
fmt 0 drift; workspace clippy 0 warnings.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Add the bounded-offer negotiation affordance: an agent under a pay-mandate makes an OFFER for a mandate-scoped asset and an injected Negotiator seller answers accept/counter/reject, closing the shop loop (quote -> negotiate -> pay). The provable property: the offer rides the signed input_hash as a canonical positive integer of spend units, and an offer above the mandate's un-spent cap (SpendMeter::remaining) is refused BEFORE it reaches the seller — an agent can never propose to commit its operator beyond granted authority. Non-value-moving by construction: it only reads the cap (no reserve, debit, or broadcast), so no money moves; settlement stays runtime.pay. A read-authority probe (like market_quote): it reconciles `read` and takes a read mandate on the pay resource — the dispatch gate authorizes only the fixed Action enum, so a made-up `negotiate` action could never be granted; the OFFER, not the action, makes it a proposal. The receipt attests the bounded offer, not the counterparty's disposition (which the runtime cannot verify); the seller's terms ride the response's disclosure channel as ephemeral market data. Production ListingNegotiator: a fixed-price DRM seller backed by the same read-only quote spine, reusing the buy gate's EXACT spend-unit conversion + pay-token guard (no second, divergent money-domain conversion). Wired only on the DRM rail (unwired on HTTP/ERC-20, which have no listing to negotiate against). Ratchet a_faithful_negotiate_returns_a_gate_legal_action_that_reconciles_matched pins the returned action to a gate-authorizable, declaration-matching value so the affordance can never regress to an action no token could authorize. Also fixes a stray SecureTimestamp clone the clippy -D warnings gate flagged in S49 test code. Full gate green: server lib 1300, dev-modes ok, bin 105, runtime 436, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…n + read-only seam contract Two-seat adversarial council: red-team SHIP-CLEAN (verified byte-identical buy-gate parity, cap-before-seller, non-value-moving, no report forgery); guardian SHIP-WITH-FIXES (3 MED + 2 LOW honesty/by-construction gaps). Both independently corroborated the pre-council action fix already landed. All folded: MED1 — the seller's rejection reason reaches the agent/logs but was unbounded (and embedded unsanitized chain-sourced token/price). The executor now bounds (<=200) and sanitizes (ascii-graphic + space) it, the same discipline the Performed report is held to. Ratchet a_hostile_seller_reject_reason_is_bounded_and_sanitized. MED2 — "reuses the buy gate's EXACT conversion" was by-comment, not by construction (a second, hand-synced copy). Extracted the ONE authorize_amount_against_listing (token guard + price parse + amount*spend_unit + cover boundary); both the DRM buy gate and the negotiate seller now call it, so an accept corresponds exactly to what the buy will pass. Buy-gate messages preserved (22 drm_marketplace tests green). MED3 — the read classification was honest only for the wired seller. Added a NORMATIVE contract to the Negotiator trait: implementations MUST be observationally read-only; an outbound-negotiating seller must reclassify the affordance action before being wired. LOW4 — the ratchet pinned a hand-copied action set. Added FromStr for Action (the one inverse of Display) and routed all three hand-rolled action matches (dispatch gate + two provisioning handlers) through parse_action; the ratchet now pins via FromStr. Round-trip test in token.rs. LOW5 — softened the panic-path comments to name the real residual: a seller that panics mid-read of the shared quote spine leaves a TTL-bounded (~30s) cache slot; money state stays clean and it is not agent-triggerable. Full gate green: server lib 1302, dev-modes 1309, bin 105, runtime 437, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…hanged (Track C2) MEASURE-FIRST. Baseline (benches/audit_emit.rs, this box): memory-only emit 2.2 us/op (~446k ops/s); file-backed fsync-per-record ~950 us/op — a ~1.1k emits/s durable ceiling at ~430x the CPU cost, with every concurrent emitter serialized behind it. That fsync-under-the-chain-lock is the KNOWN_GAPS G8 blocker keeping ordinary grant/use events best-effort. THE REWRITE (three measured cuts; the first two were rejected by their own numbers): emit now appends in chain order under the chain lock (seq + sign + ordered write + BufWriter flush), publishes written_seq, then WAITS until a flusher's sync_all covers its seq — N concurrent emits share one fsync. - Cut 1 anchored the tail-truncation head anchor under the flush lock: 1.5x. - Cut 2 fsynced through the writer mutex: appenders (holding chain, waiting on writer) convoyed behind every ~1 ms fsync — batches collapsed to ~1 and 8 threads measured SLOWER than serial (725 vs 1057 ops/s). Rejected. - Final: the flusher fsyncs a CLONED fd (fsync commits the inode, not the fd), so appends proceed during the fsync; the anchor is guarded-monotone on its own mutex off the flush path. 8 threads: 3116 emits/s — 2.9x past the single-writer ceiling (pre-S51 they were pinned AT it). Single-threaded latency unchanged (one fsync each; nothing to coalesce). THE CONTRACT IS UNCHANGED AND HARDENED: Ok(()) still means THIS record is durably fsynced. A failed write/flush/fsync now POISONS the log — the failing emit and every later one return Err (fail-closed) — which also retires the pre-S51 latent hazard where retrying a failed seq could append a duplicate seq behind half-landed bytes and corrupt the chain for verifiers. The head anchor only ever records durable covers (never over-claims). Ratchets: concurrent_emits_group_commit_and_the_chain_stays_perfect (8x25 threads -> contiguous, signed, verify_chain-clean, anchor at full count, re-opens with_file_verified) and a_failed_durable_write_poisons_the_log_ fail_closed. Bench gains an 8-thread regime that verifies the chain after timing, so its number counts correct commits only. KNOWN_GAPS G8/perf notes updated: fail-closed grant/use is now a policy decision, not a blocked one. Lanes green at commit: runtime lib 439 (all 50 audit), server lib 1302, bin 105, common 96, fmt clean. dev-modes + clippy tail and the two-seat council run next; their findings land in the fold commit. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…usher unwind guard Two-seat adversarial council on the group-commit rewrite: both seats SHIP-WITH-FIXES, and both independently CONFIRMED the core invariants sound (Ok => durably fsynced; anchor never over-claims; flusher handoff liveness; Release/Acquire cover pairing; cloned-fd fsync semantics). All findings folded: F1 (guardian HIGH == red-team F2, the one real race): the poison gate was check-then-append — an emit queued on the chain lock during another emit's write failure could re-derive the FAILED seq and append behind its half-landed bytes (duplicate-seq corruption; fail-closed in every consequence but the chain bricked, and in the worst interleaving the racer's own record could be acknowledged on a never-verifiable chain). Closed by construction: the write-failure arm poisons while STILL HOLDING the chain lock, and emit re-checks poison UNDER the chain lock (chain->flush nesting verified acyclic). Ratchet: a_poisoned_log_refuses_before_assigning_a_seq_or_writing. F2 (both seats MED): "restart re-opens from the durable prefix" was claimed, not implemented — a crash-torn tail (provably never-acknowledged: an acknowledged record's full line+\n was fsynced) made the verified open REFUSE a healthy log, and S51's standing flushed-not-fsynced batch widened the window. Closed: quarantine_torn_tail at open moves a non-newline-terminated trailing fragment to <log>.torn-tail (preserved, never destroyed) and resumes from the intact prefix; safe because it cannot remove an acknowledged record and grants a tamperer nothing beyond the anchor floor they already had via a clean line-boundary cut. Mid-file corruption still refuses (tamper). Ratchet: a_torn_tail_is_quarantined_and_the_log_resumes_from_the_durable_prefix. F3 (guardian MED / red-team LOW): a panic-poisoned flush mutex could strand flushing=true (every later custody emit blocking forever — worse than fail-closed). All flush-lock sites now recover via PoisonError::into_inner (FlushState is single-statement-writes valid), a FlusherGuard clears+poisons on unwind, and the loud tracing moved outside the lock. F4 (guardian MED, resolved BY MEASUREMENT): the empty-anchor-after-crash brick is closed by treating an EMPTY anchor as absent (skip the floor for that open, loudly). The fold's first cut ALSO fsynced the anchor temp — and the bench rejected it: a single-threaded emitter is its own flusher, so it doubled the per-record cost (~950us -> ~1.7ms). Reverted with the honest rationale (the <=20-byte single-sector payload is complete-or-empty after a crash, never partial garbage; non-empty garbage stays fail-closed). F5 (guardian LOW): anchored_seq now seeds from max(resumed head, ON-DISK anchor) so an unverified reopen of a truncated log can never regress the anchor and destroy truncation evidence. F6 (guardian LOW): measured-number drift harmonized to the S51 decision run. RESIDUAL noted in KNOWN_GAPS: dual-writer same-file (no flock) is pre-existing; advisory flock is the tracked close. Post-fold measurements (uncontended): single-writer ~874 us/op (~1.1k/s, unchanged from pre-fold); 8-thread group commit 2947 emits/s (2.6x the single-writer ceiling). Full gate green: runtime 441 (52 audit), server 1302, dev-modes 1309, bin 105, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…acket Flint scope (a) AuditLog::with_file now holds an exclusive advisory flock on <log>.lock for the log's lifetime — the SAME single-opener discipline the spend meter and payment ledger hold (council S28 F4 lineage), closing the S51 red-team dual-writer residual on the LAST unprotected durable custody store. Two live instances on one file would keep independent chain heads, mint the same seqs, and corrupt the on-disk chain (and under the S51 group commit one instance's fsync covers the sibling's uncoordinated bytes); a second opener now fails WouldBlock fail-closed. Taken BEFORE the torn-tail quarantine so racing openers cannot both mutate the file. Ratchet: a_second_live_opener_of_the_same_log_is_refused (refusal + verified-open refusal + reopen-after-drop). The gateway's lazy audit-log constructor now serializes construction (its benign both-construct race would otherwise surface a spurious flock refusal); server_infra distinguishes the already-open-elsewhere refusal from tamper in its boot error (council LOW2). (b) The dev-only echo_stdout println moved OUTSIDE the chain lock (S51 red-team F4) — a blocked stdout can no longer serialize every emit — and now echoes only records that actually committed. (c) docs/AUDITOR_PACKET.md gains section 7: the Flint mandate/money plane as the second external-audit scope (the four invariant families to attack, the two wire-format specs, reproduction commands, and the honest note that the internal two-seat council reduces engagement cost but does not replace independent assurance). Deliberately NOT touched: live-buy env wiring (cosmetic renames deferred until after the operator's real-transaction test). Council (combined seat): SHIP-CLEAN — drop-order (lock releases after the writer flushes), O_CLOEXEC fd non-inheritance, the no-real-dual-opener sweep (CLI paths never open the live log file), double-checked OnceLock init, and the packet's invariant claims all verified against source; both LOW honesty notes folded (Unix-only qualifiers; the boot-error branch). Full gate green: runtime 442 (53 audit), server 1302, dev-modes 1309, bin 105, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
… opt-in ELASTOS_AUDIT_FAIL_CLOSED_USE=1 makes EVERY ordinary capability use-record in CapabilityManager::validate BLOCKING: an act whose signed use-record cannot be durably written is DENIED (ValidationError::AuditWriteFailed) — the same "no act without a durable record" discipline affordance-consent tokens have always had (W2 step 9). This is the G8 flip the S51 group commit unblocked (concurrent validates now share fsyncs, so the cost is ~1 fsync of latency, not a throughput collapse); it ships as an OPT-IN so the default stays byte-identical best-effort — no behavior change for existing deployments ahead of the operator's live-transaction test. - The switch parse is strictness-directional and pure-function-tested: unset/0/false => off, 1/true => on, GARBAGE => ON with a loud warn (a switch whose only job is to be stricter never silently weakens). - Env read once per constructor (new / load_or_generate / with_key); with_fail_closed_use(bool) sets the policy explicitly for tests/embedders. - KNOWN ORDERING BOUND documented (same as the affordance path always had): a use-limited token's count is consumed before the record lands, so a use burned on a failed record is spent without an act — fail-closed, never a free act. - Ratchets: the_fail_closed_use_opt_in_denies_undurable_acts_and_the_default_does_not (both directions against one failing-log seam) + the_fail_closed_use_switch_never_weakens_on_garbage. - KNOWN_GAPS G8 updated: the policy flip now EXISTS as an operator choice; the remaining default-ON decision is deliberately the operator's, not a code gap. Ordinary grants already have their fail-closed surface (grant_durable, S24). Runtime lanes green at commit time (runtime 444 incl. the 2 new ratchets, common 96, fmt clean); the server lanes + workspace clippy complete in the in-flight gate before push. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…rd rationale
Combined council seat: SHIP-WITH-FIXES, doc-only — code semantics verified
correct, including that refunding a use on AuditWriteFailed would be UNSAFE
by the codebase's own BUG-4 contract (an Err emit may still have landed;
refunding could leave a durable success=true record the counter no longer
accounts for — burning keeps counter >= chain, the conservative direction).
F1 (MED): the poison-composition consequence was the load-bearing operational
fact and was stated nowhere an operator reads. Now stated plainly in the
field doc and KNOWN_GAPS: with the flag ON, one failed append/fsync poisons
the durable log and EVERY act process-wide denies until an operator restart
re-verifies the durable prefix — the deliberate posture (acts halt when the
custody plane is dead), and DoS-shaped (disk-full = capability-plane outage);
retries during the outage still burn use-limited tokens' counts.
F2 (LOW): the deny's own success=false use-record deliberately STAYS
best-effort under the flag — a deny is not an act, so a lost failure-record
is observability loss, never an unrecorded act; reasoned at
audit_validation_failure and in KNOWN_GAPS.
F4 (LOW): the memory-only claim hedged honestly ("short of a panic-poisoned
chain lock") + new ratchet the_fail_closed_use_opt_in_is_harmless_on_a_
memory_only_log (no durable writer => the flag adds no availability risk).
F3/F5: no code change — ordering bound already documented (F1's sentence adds
the retry-burn corollary); env-read-in-constructor cleared against precedent
with the with_fail_closed_use escape hatch.
Full gate green: runtime 445 (3 S53 ratchets), server 1302, dev-modes 1309,
bin 105, common 96; fmt clean; clippy -D warnings clean on the folded tree.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb # Conflicts: # elastos/crates/elastos-compute/src/providers/wasm.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.